API Dokumentation – Anlegen von Werkstattterminen

API: Werkstatttermine

Endpunkte zum Anlegen von Werkstattterminen in Werkstattkalender (calendar_timeline in Doku genannt)
sowie zur Ermittlung freier Termin-Slots. Folgt dem Auth-Muster von /api/invoices/create.

Authentifizierung
Alle Endpunkte erfordern den Parameter token mit dem gültigen
catama_public_api_token
(Systemeinstellungen > API). Bei ungueltigem Token: HTTP 401
mit { "status": 0, "message": "Invalid Token" }.

Übersicht der Endpunkte

Methode Pfad Zweck
POST /api/workshop-appointments/free-slots Freie Slots im Zeitraum berechnen, mit optionalen Filtern (Section, Mitarbeiter, Resource, Werkstattwagen).
POST /api/workshop-appointments/create Termin in calendar_timeline anlegen, optional inkl. Find-or-Create für Kunde & Fahrzeug.

Web-DEMOs

Werkstatttermin per API anlegen – Demo
Freie Werkstatt-Slots ermitteln
Slot suchen und Termin anlegen

POST /api/workshop-appointments/free-slots

Berechnet freie Termin-Slots im angegebenen Zeitraum über alle Tage hinweg.
Berücksichtigt belegte Einträge in calendar_timeline
mit Branch-Scope und optionalen Filtern.

Request-Felder

Name Typ Beschreibung
token string API-Token Pflicht
date_from string Startdatum Y-m-d Pflicht
date_to string Enddatum, max. 92 Tage Spanne Pflicht
slot_minutes integer Slot-Raster in Minuten (5–720, Default 30).
workshop_start H:i Überschreibt Systemeinstellungen -> Kalender Start (Default 07:00).
workshop_end H:i Überschreibt Systemeinstellungen -> Kalender Ende (Default 17:00).
section_id integer Direkter Filter auf Team-Name aus Kalender z.B. Werkstatt (999X für Team / users.id). Ueberschreibt department_id/assigned_user_id/section_name.
department_id integer Komfort-Filter: Team-Name aus Kalender z.B. Werkstatt wird intern zu section_id=999X gemappt.
assigned_user_id integer Komfort-Filter: users.id wird direkt als section_id verwendet.
section_name string Alternative zu Section-IDs: zuerst Team-Name aus Kalender z.B. Werkstatt (status=1), Fallback Klarname des Benutzers z.B. Max Mustermann (active=1). Mehrdeutigkeit → HTTP 404.
employee_id integer Filter auf Mitarbeiter. Matcht mechanic_id ODER section_id (Legacy-Termine speichern den User direkt in section_id).
employee_name string Alternative zu employee_id: Klarname des Benutzers z.B. Max Mustermann.
resource_id integer Filter auf Werkstattbahn / Hebebühne (ID der Resource, status=1).
resource_name string Alternative zu resource_id: resources.name.
workshop_vehicle_id integer Filter auf Werkstattwagen (mobile_ressource_id, gemappt auf resources.id type=2).
include_weekends integer 1 = Sa/So einbeziehen, 0 = ausschließen. Default aus Kalender-Range aus Systemeinstellungen.
exclude_status_ids string Komma-getrennte Status-IDs, deren Termine nicht blockieren. Default "-99" (Id des Status, -99 = storniert) (storniert).
branch_id integer Filial-ID, überschreibt Session-Default.
max_slots integer Maximalanzahl freier Slots (Default 200, Hardlimit 2000).
Slot-Logik: Pro Tag wird das Arbeitszeitfenster (workshop_start
bis workshop_end) in Slots der Länge
slot_minutes zerlegt.
Ein Slot gilt als frei, wenn kein Termin mit den aktiven Filtern den Slot überlappt
(busy.start < slot.end UND
busy.end > slot.start).

Beispiel-Aufruf

POST /api/workshop-appointments/free-slots
Content-Type: application/json

{
  "token": "xxxxxxxx",
  "date_from": "2026-05-22",
  "date_to": "2026-05-26",
  "slot_minutes": 30,
  "section_name": "Werkstatt",
  "employee_name": "Max Mustermann",
  "resource_name": "Hebebühne 1",
  "workshop_start": "08:00",
  "workshop_end": "17:00",
  "include_weekends": 0,
  "max_slots": 50
}

Erfolgsantwort (HTTP 200)

{
  "data": {
    "range": { "date_from": "2026-05-22", "date_to": "2026-05-26" },
    "slot_minutes": 30,
    "workshop_start": "08:00",
    "workshop_end": "17:00",
    "include_weekends": 0,
    "filters": {
      "section_id": 9991,
      "section_name": "Werkstatt",
      "employee_id": 42,
      "employee_name": "Max Mustermann",
      "resource_id": 3,
      "resource_name": "Hebebühne 1",
      "branch_id": 1
    },
    "count": 3,
    "slots": [
      {
        "date": "2026-05-22",
        "start_date": "2026-05-22 08:00:00",
        "end_date": "2026-05-22 08:30:00",
        "section_id": 9991,
        "section_name": "Werkstatt",
        "employee_id": 42,
        "employee_name": "Max Mustermann",
        "resource_id": 3,
        "resource_name": "Hebebühne 1"
      }
    ]
  },
  "status": 1,
  "message": "Success"
}

Fehler-Antworten

HTTP 401  { "status": 0, "message": "Invalid Token" }
HTTP 422  { "status": 0, "message": "date_from and date_to are required (Y-m-d)" }
HTTP 422  { "status": 0, "message": "date_from must be <= date_to" }
HTTP 422  { "status": 0, "message": "date range must not exceed 92 days" }
HTTP 422  { "status": 0, "message": "slot_minutes must be between 5 and 720" }
HTTP 404  { "status": 0, "message": "section_name not found: ..." }
HTTP 404  { "status": 0, "message": "employee_name not found: ..." }
HTTP 500  { "status": 0, "message": "DB connect failed" }


POST /api/workshop-appointments/create

Legt einen neuen Werkstatttermin in calendar_timeline an.
Optional können Kunde, Fahrzeug, Auftrag, Abteilung, Mitarbeiter, Werkstattbahn (Resource) und Werkstattwagen verknüpft werden.
Wenn kein customer_id bekannt ist, kann mit dem Objekt
customer ein Find‑or‑Create ausgelöst werden.
Analog für Fahrzeuge mit vehicle.

Request-Felder

Name Typ Beschreibung
token string API-Token Pflicht
title string Terminbezeichnung (event_name) Pflicht
start_date string Y-m-d H:i:s, Y-m-d H:i, d.m.Y H:i usw. Pflicht
end_date string Muss nach start_date liegen. Pflicht
customer_id integer Bestehender Kunde aus Kundendatenbank ID des Kunden.
customer object Find-or-Create, nur wenn customer_id fehlt.
vehicle_id integer Kundenfahrzeug aus Kundenfahrzeuge (Mapping mobile_id).
vehicle object Find-or-Create, nur wenn vehicle_id fehlt.
invoice_id integer Verknüpfter Auftrag/Beleg aus invoices.
section_id integer Direkter Wert (999X Team / ID des Mitarbeiters). Höchste Priorität.
department_id integer Abteilung als ID des Teams; intern als 999X gespeichert.
assigned_user_id integer Statt einer Abteilung kann ein einzelner Benutzer als section_id gespeichert werden.
section_name string Alternative zu Section-IDs (Team oder User).
employee_id integer Mitarbeiter / Mechaniker (users.id) → mechanic_id.
employee_name string Alternative zu users.id.
resource_id integer Werkstattbahn / Hebebühne aus resources (status=1).
resource_name string Alternative zu resource_id.
workshop_vehicle_id integer Werkstattwagen (resources.id, validiert).
work_type integer Arbeitsart aus planning_light_work_types.
resource_status_id integer Terminstatus (Default 0). -99 = storniert.
customer_waiting integer 1 = Kunde wartet, 0 = nein.
notes string Interne Notizen (extra_notes).
description string Beschreibung / Termininhalt.
branch_id integer Filial-ID, überschreibt Session-Default.
added_by integer Anlegender Benutzer.
check_conflicts boolean Default false. Wenn true: vor Insert prüfen, ob Slot bereits belegt ist (Section/Mitarbeiter/Resource/Werkstattwagen). Bei Konflikt → HTTP 409.

Validierung der ID-Felder:

  • section_id als Rohwert wird geprüft — akzeptiert wird entweder 999X mit X = userteams.utid (status=1) oder Benutzer ID (active=1). Ungültige Werte → HTTP 404.
  • employee_id muss aktiver Benutzer ID sein.
  • resource_id / workshop_vehicle_id müssen in resources (status=1) existieren.
  • vehicle_id aus clientmobiles; wenn customer_id übergeben, muss das Fahrzeug zu diesem Kunden gehören.
  • invoice_id wird gegen invoiceinventory::getInvoiceDataById() validiert.

Customer Find-or-Create

Wenn customer_id nicht gesetzt ist und stattdessen
customer übergeben wird, sucht das System einen passenden Kunden in
customers und legt nur dann einen neuen Datensatz an, wenn kein eindeutiger Treffer vorliegt.

Match-Reihenfolge (deterministisch, jeweils nur wenn vorherige Stufe leer):

  1. Exakter Treffer auf email, email_2 oder email_3.
  2. Telefon (normalisiert, nur Ziffern, ≥ 5 Stellen) gegen phone_mobile, phone_business, phone_private.
  3. Kombination zip + (surname oder company_name).
Feld Beschreibung
company_name / forename / surname Kunden-/Firmenname.
email / email_2 / email_3 E-Mail-Adressen.
phone_mobile / phone_business / phone_private Telefonnummern.
street / zip / city Anschrift.
country / country_id Land als Klartext (Lookup) oder direkt als ID.
customer_kind_id Kundenart (Default 1).
customer_match_mode strict (Default, 409 bei Mehrdeutigkeit) oder newest.
Antwortfeld data.customer_resolution:
provided | matched | created | none.

Vehicle Find-or-Create

Wenn vehicle_id nicht gesetzt ist und stattdessen
vehicle übergeben wird, sucht das System ein passendes Fahrzeug in
clientmobiles und legt es nur dann neu an, wenn kein eindeutiger Treffer vorliegt.

  1. Exakter Treffer auf chassis_number (VIN), optional eingeschränkt auf customer_id bzw. current_owner_id.
  2. Exakter Treffer auf mobile_sign (Kennzeichen), analog kunden-eingeschränkt.

Mindestens eines von chassis_number oder
mobile_sign ist erforderlich.

Feld Beschreibung
chassis_number Fahrgestellnummer / VIN.
mobile_sign Amtliches Kennzeichen.
fabricator Hersteller als Klartext (z. B. "Volkswagen") → auf Hersteller.ID aufgelöst.
fabricator_id Direkte ID, übersteuert fabricator.
modell / name / color / mileage Modellname, Farbe, Kilometerstand.
ez Erstzulassung (Y-m-d, d.m.Y oder Unix-TS).
vehicle_key_code / vehicle_registration_code HSN / TSN (KBA-Schlüsselnummern).
mobile_kind Fahrzeugart (Default 1 = PKW).
vehicle_match_mode strict (Default) oder newest.
create_if_not_found Default true. Auf false: bei Miss HTTP 404 statt Anlage.
Antwortfeld data.vehicle_resolution:
provided | matched | created | none.
Zusätzlich gibt es vehicle_label, vehicle_sign, vehicle_vin als lesbaren Kontext.

Beispiel-Aufruf (JSON)

POST /api/workshop-appointments/create
Content-Type: application/json

{
  "token": "xxxxxxxx",
  "title": "Inspektion 30tkm",
  "start_date": "2026-05-22 09:00:00",
  "end_date":   "2026-05-22 11:00:00",
  "invoice_id": 14074,
  "department_id": 1,
  "employee_name": "Max Mustermann",
  "resource_name": "Hebebühne 1",
  "work_type": 2,
  "notes": "Kunde bringt Schlüssel morgens",
  "check_conflicts": true,
  "customer": {
    "email": "max.mustermann@example.com",
    "forename": "Max",
    "surname": "Mustermann",
    "zip": "80331",
    "city": "München",
    "street": "Musterstr. 1",
    "phone_mobile": "+49 170 1234567",
    "customer_match_mode": "strict"
  },
  "vehicle": {
    "chassis_number": "WVWZZZ3CZWE123456",
    "mobile_sign": "B-AB 1234",
    "fabricator": "Volkswagen",
    "modell": "Golf VII",
    "ez": "2018-03-15",
    "mileage": 85000,
    "vehicle_match_mode": "strict"
  }
}

Erfolgsantwort (HTTP 200)

{
  "data": {
    "event_id": 5821,
    "title": "Inspektion 30tkm",
    "start_date": "2026-05-22 09:00:00",
    "end_date":   "2026-05-22 11:00:00",
    "customer_id": 12034,
    "customer_resolution": "matched",
    "vehicle_id": 1085,
    "vehicle_resolution": "matched",
    "vehicle_label": "Volkswagen Golf VII (B-AB 1234)",
    "vehicle_sign": "B-AB 1234",
    "vehicle_vin":  "WVWZZZ3CZWE123456",
    "invoice_id": 14074,
    "section_id": 9991,
    "section_name": "Werkstatt",
    "department_id": 1,
    "employee_id": 42,
    "employee_name": "Max Mustermann",
    "resource_id": 3,
    "resource_name": "Hebebühne 1",
    "work_type": 2,
    "branch_id": 1,
    "check_conflicts": 1
  },
  "status": 1,
  "message": "Success"
}

Fehler-Antworten (Auswahl)

HTTP 401  { "status": 0, "message": "Invalid Token" }
HTTP 422  { "status": 0, "message": "title is required" }
HTTP 422  { "status": 0, "message": "start_date must be before end_date" }
HTTP 404  { "status": 0, "message": "Customer not found" }
HTTP 404  { "status": 0, "message": "Vehicle not found" }
HTTP 404  { "status": 0, "message": "Invalid section_id (no matching active team or user)" }
HTTP 404  { "status": 0, "message": "employee_name not found: ..." }
HTTP 404  { "status": 0, "message": "Workshop vehicle (resources.id) not found or inactive" }
HTTP 422  { "status": 0, "message": "Vehicle does not belong to provided customer" }
HTTP 409  { "status": 0, "message": "Ambiguous customer match: 3 candidates",
            "data": { "candidates": [12034, 12089, 12190] } }
HTTP 409  { "status": 0, "message": "Ambiguous vehicle match: 2 candidates",
            "data": { "vehicle_candidates": [1085, 1240], "vehicle_match_strategy": "chassis_number" } }
HTTP 409  { "status": 0, "message": "Slot conflict detected (check_conflicts=true)",
            "data": { "conflicts": [ {"event_id": 5810, "start_date": "...", "end_date": "..."} ] } }
HTTP 500  { "status": 0, "message": "Failed to create customer (check PHP error log for SQL details)" }
HTTP 500  { "status": 0, "message": "Failed to insert appointment: <sql error>" }

Field Mapping — calendar_timeline

API-Feld DB-Spalte Bemerkung
title event_name String
start_date / end_date start_date / end_date DATETIME
customer_id customer_id customers.id
vehicle_id / vehicle mobile_id clientmobiles.id (Find-or-Create über VIN/Kennzeichen)
invoice_id invoice_id invoices.id
section_id / department_id / assigned_user_id / section_name section_id 999X = Team (utid), sonst direkter users.id
employee_id / employee_name mechanic_id users.id (Mechaniker)
resource_id / resource_name resource_id Werkstattbahn / Hebebühne
workshop_vehicle_id mobile_ressource_id Werkstattwagen (resources.id, type=2)
work_type work_type planning_light_work_types.id
resource_status_id resource_status_id Terminstatus (-99 = storniert)
customer_waiting customer_waiting 0/1
notes extra_notes Interne Notizen
description description Termininhalt
branch_id branch_id Filiale
added_by added_by / updated_by users.id

Race Conditions & Hinweise

Bekannte Race-Windows

  • Free-Slots → Create: Zwischen dem Ergebnis von /free-slots und dem Aufruf von /create kann ein anderer Aufrufer denselben Slot reservieren. Mit check_conflicts=true wird das Risiko stark reduziert (best-effort).
  • Customer Find-or-Create: Ohne UNIQUE-Constraint auf customers.email können zwei parallele Calls beide den Match verfehlen und je einen Datensatz anlegen. Empfehlung: Calls je Kunde sequenziell auslösen oder UNIQUE-Index pflegen.
  • Vehicle Find-or-Create: Analog — ohne UNIQUE auf chassis_number/mobile_sign sind in seltenen Fällen Doppel-Inserts möglich.
  • Soft-deleted Fahrzeuge: Match filtert auf mobile_status=1; archivierte Fahrzeuge gelten als „nicht vorhanden“ und werden ggf. neu angelegt.
Zeitzonen: start_date/end_date werden in der lokalen Server-Zeitzone (PHP-Default) verarbeitet und so in calendar_timeline gespeichert. Externe Aufrufer sollten dieselbe Zeitzone verwenden wie der Server.

Empfohlener Workflow

  1. Mit POST /api/workshop-appointments/free-slots freie Slots für den gewünschten Mitarbeiter / die Werkstattbahn ermitteln.
  2. Vom Anwender den Slot auswählen lassen.
  3. Mit POST /api/workshop-appointments/create den Termin anlegen — idealerweise mit check_conflicts=true, damit konkurrierende Buchungen ein HTTP 409 auslösen.
  4. Bei 409 Slot conflict detected: Slots erneut abfragen und neu anbieten.

Ähnliche Artikel