{
  "openapi": "3.1.0",
  "info": {
    "title": "SolvePlus (S+) Partner-API",
    "version": "1.0.0",
    "summary": "Lesender Zugriff für Software-Partner auf Daten ihrer gemeinsamen Kunden – per OAuth 2.0 mit PKCE.",
    "description": "Die Partner-API erlaubt freigegebenen Software-Anbietern lesenden Zugriff auf Termine, Schichten, Zeiteinträge, Rapporte, Kunden/Einsatzorte und Personal-Basisdaten eines Mandanten – ausschließlich nach Zustimmung des Inhabers (OAuth 2.0 Authorization Code + PKCE S256).\n\nHinweis: Diese Spezifikation ist Version 1 im Aufbau. Einzelne Felder können sich noch an die Backend-Implementierung angleichen; maßgeblich ist die tatsächliche Antwort. Neue optionale Felder gelten nicht als inkompatible Änderung – ignorieren Sie unbekannte Felder.\n\nVersionierung: Die Hauptversion steht im Pfad (/v1). Inkompatible Änderungen gibt es nur in einer neuen Hauptversion. Abgekündigte Endpunkte senden die Header Deprecation und Sunset; zwischen Abkündigung und Abschaltung liegen mindestens 6 Monate.\n\nDie Nutzung ist kostenpflichtig und nur für Unternehmen (B2B). Vor dem Live-Betrieb ist die Sandbox Pflicht.",
    "contact": {
      "name": "SolvePlus Partner-Support",
      "url": "https://splus-app.de/entwickler"
    },
    "termsOfService": "https://splus-app.de/agb.html"
  },
  "servers": [
    {
      "url": "https://splus-app.de/api/partner/v1",
      "description": "Produktion und Sandbox (die Umgebung ergibt sich aus der App)."
    }
  ],
  "tags": [
    {
      "name": "OAuth",
      "description": "Token holen und widerrufen."
    },
    {
      "name": "Allgemein"
    },
    {
      "name": "Termine"
    },
    {
      "name": "Schichten"
    },
    {
      "name": "Zeiteinträge"
    },
    {
      "name": "Rapporte"
    },
    {
      "name": "Kunden"
    },
    {
      "name": "Personal"
    },
    {
      "name": "Webhooks",
      "description": "Ausgehende Benachrichtigungen an Ihre Ziel-URL."
    }
  ],
  "security": [
    {
      "oauth2": []
    }
  ],
  "paths": {
    "/oauth/token": {
      "post": {
        "operationId": "tokenHolen",
        "tags": [
          "OAuth"
        ],
        "summary": "Code oder Refresh-Token gegen Tokens tauschen",
        "security": [],
        "description": "grant_type=authorization_code (mit PKCE-code_verifier) oder grant_type=refresh_token. Refresh-Tokens rotieren: Wird ein bereits ersetzter Refresh-Token erneut benutzt, wird die ganze Token-Kette widerrufen (Schutz bei Diebstahl).",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TokenAnfrageCode"
                  },
                  {
                    "$ref": "#/components/schemas/TokenAnfrageRefresh"
                  }
                ]
              }
            },
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TokenAnfrageCode"
                  },
                  {
                    "$ref": "#/components/schemas/TokenAnfrageRefresh"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenAntwort"
                }
              }
            }
          },
          "400": {
            "description": "OAuth-Fehler nach RFC 6749 (invalid_grant, invalid_client, invalid_request, unsupported_grant_type).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "error_description": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "invalid_client",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          }
        }
      }
    },
    "/oauth/revoke": {
      "post": {
        "operationId": "tokenWiderrufen",
        "tags": [
          "OAuth"
        ],
        "summary": "Token widerrufen (RFC 7009)",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/RevokeAnfrage"
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RevokeAnfrage"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Immer 200 – auch bei unbekanntem Token (kein Rückschluss möglich)."
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          }
        }
      }
    },
    "/ich": {
      "get": {
        "operationId": "ich",
        "tags": [
          "Allgemein"
        ],
        "summary": "App, Mandant und wirksame Scopes zum Token",
        "security": [
          {
            "oauth2": []
          }
        ],
        "responses": {
          "200": {
            "description": "Info",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ich"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/termine": {
      "get": {
        "operationId": "termineListe",
        "tags": [
          "Termine"
        ],
        "summary": "Termine auflisten",
        "description": "Termine des Mandanten (von/bis beziehen sich auf beginn). Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "kalender.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          },
          {
            "$ref": "#/components/parameters/Von"
          },
          {
            "$ref": "#/components/parameters/Bis"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/termine/{id}": {
      "get": {
        "operationId": "terminLesen",
        "tags": [
          "Termine"
        ],
        "summary": "Einen Termin lesen",
        "security": [
          {
            "oauth2": [
              "kalender.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Der Datensatz",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminEinzeln"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/schichten": {
      "get": {
        "operationId": "schichtenListe",
        "tags": [
          "Schichten"
        ],
        "summary": "Schichten auflisten",
        "description": "Geplante Schichten des Mandanten. Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "schichten.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SchichtListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/zeiteintraege": {
      "get": {
        "operationId": "zeiteintraegeListe",
        "tags": [
          "Zeiteinträge"
        ],
        "summary": "Zeiteinträge auflisten",
        "description": "Erfasste Arbeitszeiten (von/bis beziehen sich auf datum). Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "zeit.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          },
          {
            "$ref": "#/components/parameters/Von"
          },
          {
            "$ref": "#/components/parameters/Bis"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ZeiteintragListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/rapporte": {
      "get": {
        "operationId": "rapporteListe",
        "tags": [
          "Rapporte"
        ],
        "summary": "Rapporte auflisten",
        "description": "Tagesberichte (von/bis beziehen sich auf datum). Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "rapporte.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          },
          {
            "$ref": "#/components/parameters/Von"
          },
          {
            "$ref": "#/components/parameters/Bis"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RapportListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/rapporte/{id}": {
      "get": {
        "operationId": "rapportLesen",
        "tags": [
          "Rapporte"
        ],
        "summary": "Einen Rapport lesen",
        "security": [
          {
            "oauth2": [
              "rapporte.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Der Datensatz",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RapportEinzeln"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/kunden": {
      "get": {
        "operationId": "kundenListe",
        "tags": [
          "Kunden"
        ],
        "summary": "Kunden auflisten",
        "description": "Kunden des Mandanten (Einsatzorte ohne übergeordneten Kunden). Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "kunden.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/KundeListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/einsatzorte": {
      "get": {
        "operationId": "einsatzorteListe",
        "tags": [
          "Kunden"
        ],
        "summary": "Einsatzorte auflisten",
        "description": "Einsatzorte (Objekte) der Kunden. Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "kunden.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EinsatzortListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    },
    "/personal": {
      "get": {
        "operationId": "personalListe",
        "tags": [
          "Personal"
        ],
        "summary": "Personal auflisten (Basisdaten)",
        "description": "Nur Basisdaten, keine sensiblen Felder. Hinweis: Personal hat kein geaendert_am im Ergebnis; aenderung_seit filtert trotzdem serverseitig. Sortierung: nach id; mit aenderung_seit nach (geaendert_am, id) – stabil für den Cursor, ohne Doppelte oder Lücken.",
        "security": [
          {
            "oauth2": [
              "personal.lesen"
            ]
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/AenderungSeit"
          },
          {
            "$ref": "#/components/parameters/MitArchivierten"
          }
        ],
        "responses": {
          "200": {
            "description": "Eine Seite",
            "headers": {
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/X-RateLimit-Limit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/X-RateLimit-Remaining"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PersonalListe"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "422": {
            "$ref": "#/components/responses/Validation"
          },
          "429": {
            "$ref": "#/components/responses/TooMany"
          },
          "503": {
            "$ref": "#/components/responses/Unavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "termin.erstellt": {
      "post": {
        "operationId": "webhook_termin_erstellt",
        "tags": [
          "Webhooks"
        ],
        "summary": "termin.erstellt",
        "description": "Ein Termin wurde angelegt. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "termin.erstellt",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "termin",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "termin.geaendert": {
      "post": {
        "operationId": "webhook_termin_geaendert",
        "tags": [
          "Webhooks"
        ],
        "summary": "termin.geaendert",
        "description": "Ein Termin wurde geändert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "termin.geaendert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "termin",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "termin.storniert": {
      "post": {
        "operationId": "webhook_termin_storniert",
        "tags": [
          "Webhooks"
        ],
        "summary": "termin.storniert",
        "description": "Ein Termin wurde storniert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "termin.storniert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "termin",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "zeiteintrag.erstellt": {
      "post": {
        "operationId": "webhook_zeiteintrag_erstellt",
        "tags": [
          "Webhooks"
        ],
        "summary": "zeiteintrag.erstellt",
        "description": "Ein Zeiteintrag wurde angelegt. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "zeiteintrag.erstellt",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "zeiteintrag",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "zeiteintrag.geaendert": {
      "post": {
        "operationId": "webhook_zeiteintrag_geaendert",
        "tags": [
          "Webhooks"
        ],
        "summary": "zeiteintrag.geaendert",
        "description": "Ein Zeiteintrag wurde geändert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "zeiteintrag.geaendert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "zeiteintrag",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "rapport.erstellt": {
      "post": {
        "operationId": "webhook_rapport_erstellt",
        "tags": [
          "Webhooks"
        ],
        "summary": "rapport.erstellt",
        "description": "Ein Rapport wurde angelegt. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "rapport.erstellt",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "rapport",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "rapport.geaendert": {
      "post": {
        "operationId": "webhook_rapport_geaendert",
        "tags": [
          "Webhooks"
        ],
        "summary": "rapport.geaendert",
        "description": "Ein Rapport wurde geändert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "rapport.geaendert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "rapport",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "kunde.geaendert": {
      "post": {
        "operationId": "webhook_kunde_geaendert",
        "tags": [
          "Webhooks"
        ],
        "summary": "kunde.geaendert",
        "description": "Ein Kunde wurde geändert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "kunde.geaendert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "kunde",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    },
    "einsatzort.geaendert": {
      "post": {
        "operationId": "webhook_einsatzort_geaendert",
        "tags": [
          "Webhooks"
        ],
        "summary": "einsatzort.geaendert",
        "description": "Ein Einsatzort wurde geändert. Antworten Sie schnell mit 2xx; alles andere gilt als Fehlschlag und wird mit wachsendem Abstand wiederholt.",
        "parameters": [
          {
            "name": "X-SPlus-Signatur",
            "in": "header",
            "required": true,
            "description": "t=<Unix-Zeit>,v1=<hex HMAC-SHA256 über \"<t>.<roher Body>\" mit dem Webhook-Geheimnis>. Zeittoleranz 5 Minuten; Vergleich in konstanter Zeit.",
            "schema": {
              "type": "string",
              "examples": [
                "t=1767225600,v1=5f2b…"
              ]
            }
          },
          {
            "name": "X-SPlus-Ereignis",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "termin.erstellt",
                "termin.geaendert",
                "termin.storniert",
                "zeiteintrag.erstellt",
                "zeiteintrag.geaendert",
                "rapport.erstellt",
                "rapport.geaendert",
                "kunde.geaendert",
                "einsatzort.geaendert"
              ]
            }
          },
          {
            "name": "X-SPlus-Zustellung",
            "in": "header",
            "required": true,
            "description": "ID dieses Zustellversuchs (bei Wiederholung neu; die Ereignis-ID im Body bleibt gleich).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEreignis"
              },
              "examples": {
                "beispiel": {
                  "value": {
                    "id": "evt_01J9…",
                    "ereignis": "einsatzort.geaendert",
                    "erstellt_am": "2026-09-29T08:15:00Z",
                    "mandant": {
                      "firmen_id": "SPL-AB12-CD34"
                    },
                    "objekt": {
                      "typ": "einsatzort",
                      "id": 1042
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Empfang bestätigt."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "oauth2": {
        "type": "oauth2",
        "description": "Authorization Code mit PKCE (S256, Pflicht). Access-Token 15 Minuten, Refresh-Token 30 Tage rotierend. Redirect-URIs nur https und exakt wie hinterlegt.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://splus-app.de/oauth/autorisieren",
            "tokenUrl": "https://splus-app.de/api/partner/v1/oauth/token",
            "refreshUrl": "https://splus-app.de/api/partner/v1/oauth/token",
            "scopes": {
              "kalender.lesen": "Termine / Kalender lesen",
              "schichten.lesen": "Schichten lesen",
              "zeit.lesen": "Zeiteinträge lesen",
              "rapporte.lesen": "Rapporte lesen",
              "kunden.lesen": "Kunden und Einsatzorte lesen",
              "personal.lesen": "Personal (Basisdaten, ohne sensible Angaben) lesen",
              "termine.schreiben": "Termine anlegen/ändern – vorbereitet, nur mit gesonderter Betreiber-Freigabe, noch keine Endpunkte",
              "zeit.schreiben": "Zeiteinträge anlegen – vorbereitet, nur mit gesonderter Betreiber-Freigabe, noch keine Endpunkte",
              "rapporte.schreiben": "Rapporte anlegen – vorbereitet, nur mit gesonderter Betreiber-Freigabe, noch keine Endpunkte"
            }
          }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "Fehler nach RFC 9457 (application/problem+json).",
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://splus-app.de/partner-api/fehler#scope_fehlt"
            ]
          },
          "title": {
            "type": "string",
            "examples": [
              "Scope fehlt"
            ]
          },
          "status": {
            "type": "integer",
            "examples": [
              403
            ]
          },
          "detail": {
            "type": "string",
            "examples": [
              "Für diesen Endpunkt ist der Scope kalender.lesen nötig."
            ]
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_token",
              "scope_fehlt",
              "app_gesperrt",
              "partner_gesperrt",
              "mandant_nicht_erlaubt",
              "ip_nicht_erlaubt",
              "nicht_gefunden",
              "validierung",
              "rate_limit",
              "kontingent_erschoepft",
              "mandant_gesperrt",
              "partner_api_aus"
            ]
          },
          "instance": {
            "type": "string",
            "description": "Optional: Kennung dieses Vorfalls (für Support-Anfragen)."
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Nur bei validierung: Feld → Meldungen."
          }
        }
      },
      "Termin": {
        "type": "object",
        "required": [
          "id",
          "beginn",
          "status",
          "geaendert_am"
        ],
        "description": "Ohne Notizen, interne Notizen, Treffpunkte/GPS und Absagegründe.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "einsatzort_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "mitarbeiter_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "beginn": {
            "type": "string",
            "format": "date-time",
            "description": "Beginn"
          },
          "ende": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "examples": [
              "planned",
              "confirmed",
              "done",
              "cancelled"
            ]
          },
          "abgesagt": {
            "type": "boolean"
          },
          "archiviert": {
            "type": "boolean",
            "description": "Nur mit ?mit_archivierten=1 überhaupt enthalten."
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "Schicht": {
        "type": "object",
        "required": [
          "id",
          "aktiv",
          "geaendert_am"
        ],
        "description": "Wiederkehrende Einteilung (Wochentage + Uhrzeit, gültig ab/bis).",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "einsatzort_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "mitarbeiter_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "wochentage": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 7
            },
            "description": "1 = Montag … 7 = Sonntag"
          },
          "gilt_ab": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Datum YYYY-MM-DD"
          },
          "gilt_bis": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Datum YYYY-MM-DD"
          },
          "beginn": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Uhrzeit HH:MM (Ortszeit des Mandanten)",
            "examples": [
              "07:30"
            ]
          },
          "ende": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Uhrzeit HH:MM (Ortszeit des Mandanten)",
            "examples": [
              "07:30"
            ]
          },
          "aktiv": {
            "type": "boolean"
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "Zeiteintrag": {
        "type": "object",
        "required": [
          "id",
          "mitarbeiter_id",
          "datum",
          "geaendert_am"
        ],
        "description": "Ohne GPS, Geräte-IDs und Notizen.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "mitarbeiter_id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "einsatzort_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "datum": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Datum YYYY-MM-DD"
          },
          "beginn": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Uhrzeit HH:MM (Ortszeit des Mandanten)",
            "examples": [
              "07:30"
            ]
          },
          "ende": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "HH:MM; null = läuft noch",
            "examples": [
              "07:30"
            ]
          },
          "pause_minuten": {
            "type": "integer",
            "minimum": 0
          },
          "netto_minuten": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "description": "z. B. offen, abgeschlossen, storniert (storniert nur mit ?mit_archivierten=1)"
          },
          "freigabe": {
            "type": [
              "string",
              "null"
            ]
          },
          "erfassungsart": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "qr",
              "manuell"
            ]
          },
          "art": {
            "type": [
              "string",
              "null"
            ]
          },
          "fahrzeit_minuten": {
            "type": [
              "integer",
              "null"
            ]
          },
          "fahrt_km": {
            "type": [
              "number",
              "null"
            ]
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "RapportBesatzung": {
        "type": "object",
        "properties": {
          "mitarbeiter_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "qualifikation": {
            "type": [
              "string",
              "null"
            ]
          },
          "anzahl": {
            "type": "integer"
          },
          "stunden": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "Rapport": {
        "type": "object",
        "required": [
          "id",
          "mitarbeiter_id",
          "datum",
          "status",
          "geaendert_am"
        ],
        "description": "Ohne Freitexte (Bemerkung, Besonderheiten, Kundenrückmeldung), Unterschriften (nur vorhanden ja/nein) und Fotos.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "mitarbeiter_id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "einsatzort_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "termin_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "zeiteintrag_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "datum": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Datum YYYY-MM-DD"
          },
          "beginn": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Uhrzeit HH:MM (Ortszeit des Mandanten)",
            "examples": [
              "07:30"
            ]
          },
          "ende": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-2][0-9]:[0-5][0-9]$",
            "description": "Uhrzeit HH:MM (Ortszeit des Mandanten)",
            "examples": [
              "07:30"
            ]
          },
          "pause_minuten": {
            "type": "integer",
            "minimum": 0
          },
          "status": {
            "type": "string",
            "description": "archiviert nur mit ?mit_archivierten=1"
          },
          "art": {
            "type": [
              "string",
              "null"
            ]
          },
          "flaeche_qm": {
            "type": [
              "number",
              "null"
            ]
          },
          "notdienst": {
            "type": "boolean"
          },
          "unterschrift_kunde_vorhanden": {
            "type": "boolean"
          },
          "unterschrift_mitarbeiter_vorhanden": {
            "type": "boolean"
          },
          "besatzung": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RapportBesatzung"
            }
          },
          "eingegangen_am": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "geprueft_am": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "Kunde": {
        "type": "object",
        "required": [
          "id",
          "name",
          "aktiv"
        ],
        "description": "Ohne Anschrift, GPS, QR-Token, Hinweise und Rechnungsangaben.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "name": {
            "type": "string"
          },
          "aktiv": {
            "type": "boolean"
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "Einsatzort": {
        "type": "object",
        "required": [
          "id",
          "name",
          "aktiv"
        ],
        "description": "Objekt/Einsatzort. Ohne Anschrift und GPS.",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "name": {
            "type": "string"
          },
          "kunde_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "null = Kunde selbst bzw. freies Objekt"
          },
          "aktiv": {
            "type": "boolean"
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time",
            "description": "Anlagezeitpunkt (ISO-8601 mit Zeitzone)."
          },
          "geaendert_am": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt der letzten Änderung (ISO-8601 mit Zeitzone). Grundlage für aenderung_seit."
          }
        }
      },
      "Personal": {
        "type": "object",
        "required": [
          "id",
          "anzeigename",
          "aktiv"
        ],
        "description": "Nur Basisdaten. Keine sensiblen Felder (kein Geburtsdatum, keine Anschrift, kein Lohn, keine Telefonnummer, keine E-Mail, keine Personalnummer).",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Stabile numerische ID.",
            "examples": [
              1042
            ]
          },
          "anzeigename": {
            "type": "string",
            "examples": [
              "Anna Muster"
            ]
          },
          "rolle": {
            "type": [
              "string",
              "null"
            ]
          },
          "aktiv": {
            "type": "boolean"
          }
        }
      },
      "Ich": {
        "type": "object",
        "properties": {
          "app": {
            "type": "object",
            "properties": {
              "client_id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "status": {
                "type": "string",
                "examples": [
                  "aktiv",
                  "gedrosselt"
                ]
              }
            }
          },
          "partner": {
            "type": "object",
            "properties": {
              "partner_nummer": {
                "type": "string"
              },
              "firma": {
                "type": "string"
              }
            }
          },
          "mandant": {
            "type": "object",
            "properties": {
              "firmen_id": {
                "type": "string",
                "examples": [
                  "SPL-AB12-CD34"
                ]
              },
              "name": {
                "type": "string"
              }
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Wirksame Scopes = Token ∩ App-Freigabe ∩ Zustimmung des Inhabers."
          },
          "umgebung": {
            "type": "string",
            "enum": [
              "sandbox",
              "live"
            ]
          }
        }
      },
      "TokenAntwort": {
        "type": "object",
        "required": [
          "access_token",
          "token_type",
          "expires_in",
          "refresh_token",
          "scope"
        ],
        "properties": {
          "access_token": {
            "type": "string",
            "examples": [
              "spa_…"
            ]
          },
          "token_type": {
            "type": "string",
            "const": "Bearer"
          },
          "expires_in": {
            "type": "integer",
            "examples": [
              900
            ],
            "description": "Sekunden (15 Minuten)."
          },
          "refresh_token": {
            "type": "string",
            "examples": [
              "spr_…"
            ],
            "description": "30 Tage gültig, rotierend: jede Nutzung liefert ein neues; das alte wird ungültig."
          },
          "scope": {
            "type": "string",
            "examples": [
              "kalender.lesen zeit.lesen"
            ]
          }
        }
      },
      "TokenAnfrageCode": {
        "type": "object",
        "required": [
          "grant_type",
          "code",
          "redirect_uri",
          "client_id",
          "client_secret",
          "code_verifier"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "authorization_code"
          },
          "code": {
            "type": "string"
          },
          "redirect_uri": {
            "type": "string",
            "format": "uri"
          },
          "client_id": {
            "type": "string",
            "examples": [
              "spc_…"
            ]
          },
          "client_secret": {
            "type": "string"
          },
          "code_verifier": {
            "type": "string",
            "minLength": 43,
            "maxLength": 128
          }
        }
      },
      "TokenAnfrageRefresh": {
        "type": "object",
        "required": [
          "grant_type",
          "refresh_token",
          "client_id",
          "client_secret"
        ],
        "properties": {
          "grant_type": {
            "type": "string",
            "const": "refresh_token"
          },
          "refresh_token": {
            "type": "string"
          },
          "client_id": {
            "type": "string"
          },
          "client_secret": {
            "type": "string"
          }
        }
      },
      "RevokeAnfrage": {
        "type": "object",
        "required": [
          "token",
          "client_id",
          "client_secret"
        ],
        "properties": {
          "token": {
            "type": "string",
            "description": "Access- oder Refresh-Token. Ein Refresh-Token widerruft die ganze Kette."
          },
          "client_id": {
            "type": "string"
          },
          "client_secret": {
            "type": "string"
          }
        }
      },
      "WebhookEreignis": {
        "type": "object",
        "required": [
          "id",
          "ereignis",
          "erstellt_am",
          "mandant",
          "objekt"
        ],
        "description": "„Thin webhook\": nur IDs und Typ. Details holen Sie mit dem Access-Token über die API (dabei greifen Scopes und Zustimmung erneut).",
        "properties": {
          "id": {
            "type": "string",
            "description": "Eindeutige Ereignis-ID (für Idempotenz – doppelte Zustellung möglich)."
          },
          "ereignis": {
            "type": "string",
            "enum": [
              "termin.erstellt",
              "termin.geaendert",
              "termin.storniert",
              "zeiteintrag.erstellt",
              "zeiteintrag.geaendert",
              "rapport.erstellt",
              "rapport.geaendert",
              "kunde.geaendert",
              "einsatzort.geaendert"
            ]
          },
          "erstellt_am": {
            "type": "string",
            "format": "date-time"
          },
          "mandant": {
            "type": "object",
            "required": [
              "firmen_id"
            ],
            "properties": {
              "firmen_id": {
                "type": "string",
                "examples": [
                  "SPL-AB12-CD34"
                ]
              }
            }
          },
          "objekt": {
            "type": "object",
            "required": [
              "typ",
              "id"
            ],
            "properties": {
              "typ": {
                "type": "string",
                "enum": [
                  "termin",
                  "zeiteintrag",
                  "rapport",
                  "kunde",
                  "einsatzort"
                ]
              },
              "id": {
                "type": "integer"
              }
            }
          }
        }
      },
      "TerminListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Termin"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "SchichtListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Schicht"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "ZeiteintragListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Zeiteintrag"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "RapportListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rapport"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "KundeListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Kunde"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "EinsatzortListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Einsatzort"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "PersonalListe": {
        "type": "object",
        "required": [
          "daten",
          "naechster_cursor"
        ],
        "properties": {
          "daten": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Personal"
            }
          },
          "naechster_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Für die nächste Seite als ?cursor= übergeben; null = letzte Seite."
          }
        }
      },
      "TerminEinzeln": {
        "type": "object",
        "required": [
          "daten"
        ],
        "properties": {
          "daten": {
            "$ref": "#/components/schemas/Termin"
          }
        }
      },
      "RapportEinzeln": {
        "type": "object",
        "required": [
          "daten"
        ],
        "properties": {
          "daten": {
            "$ref": "#/components/schemas/Rapport"
          }
        }
      }
    },
    "parameters": {
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "Undurchsichtiger Cursor aus naechster_cursor der vorigen Seite.",
        "schema": {
          "type": "string"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Datensätze je Seite (1–100).",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100,
          "default": 50
        }
      },
      "AenderungSeit": {
        "name": "aenderung_seit",
        "in": "query",
        "required": false,
        "description": "Nur Datensätze mit geaendert_am >= diesem Zeitpunkt (ISO-8601, z. B. 2026-09-01T00:00:00Z). Sortierung dann nach (geaendert_am, id). Für inkrementellen Abgleich.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Positive ganze Zahl; alles andere oder fremde IDs → 404 nicht_gefunden.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "MitArchivierten": {
        "name": "mit_archivierten",
        "in": "query",
        "required": false,
        "description": "1 = auch archivierte/stornierte/inaktive Datensätze liefern (mit Kennzeichen). Standard 0.",
        "schema": {
          "type": "string",
          "enum": [
            "0",
            "1",
            "true",
            "false"
          ],
          "default": "0"
        }
      },
      "Von": {
        "name": "von",
        "in": "query",
        "required": false,
        "description": "Ab Datum (YYYY-MM-DD, inklusiv).",
        "schema": {
          "type": "string",
          "format": "date"
        }
      },
      "Bis": {
        "name": "bis",
        "in": "query",
        "required": false,
        "description": "Bis Datum (YYYY-MM-DD, inklusiv).",
        "schema": {
          "type": "string",
          "format": "date"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "RESERVIERT für künftige schreibende Endpunkte: eindeutiger Schlüssel (z. B. UUID) je Vorgang; eine Wiederholung mit gleichem Schlüssel liefert dieselbe Antwort statt einer Doppelbuchung. Bei lesenden Endpunkten ohne Wirkung.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      }
    },
    "headers": {
      "X-RateLimit-Limit": {
        "description": "Erlaubte Anfragen pro Minute (Tarif bzw. App-Einstellung).",
        "schema": {
          "type": "integer"
        }
      },
      "X-RateLimit-Remaining": {
        "description": "Verbleibende Anfragen im aktuellen Minutenfenster.",
        "schema": {
          "type": "integer"
        }
      },
      "Retry-After": {
        "description": "Sekunden bis zum nächsten Versuch (bei 429/503).",
        "schema": {
          "type": "integer"
        }
      },
      "Deprecation": {
        "description": "Gesetzt, wenn der Endpunkt abgekündigt ist (RFC 9745, z. B. „@1767225600\").",
        "schema": {
          "type": "string"
        }
      },
      "Sunset": {
        "description": "Datum, ab dem der abgekündigte Endpunkt entfällt (RFC 8594, HTTP-Datum).",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "invalid_token",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Forbidden": {
        "description": "scope_fehlt, app_gesperrt, partner_gesperrt, mandant_nicht_erlaubt, ip_nicht_erlaubt",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "nicht_gefunden",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "Validation": {
        "description": "validierung",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "TooMany": {
        "description": "rate_limit oder kontingent_erschoepft",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        }
      },
      "Unavailable": {
        "description": "mandant_gesperrt oder partner_api_aus",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        }
      }
    }
  },
  "x-fehlercodes": [
    {
      "code": "invalid_token",
      "status": 401,
      "beschreibung": "Access-Token fehlt, ist abgelaufen, widerrufen oder unbekannt. Mit dem Refresh-Token ein neues holen.",
      "type": "https://splus-app.de/partner-api/fehler#invalid_token"
    },
    {
      "code": "scope_fehlt",
      "status": 403,
      "beschreibung": "Der Token hat den nötigen Scope nicht (oder er ist für die App bzw. vom Mandanten nicht freigegeben).",
      "type": "https://splus-app.de/partner-api/fehler#scope_fehlt"
    },
    {
      "code": "app_gesperrt",
      "status": 403,
      "beschreibung": "Die App ist gesperrt (Betreiber, Zahlung, Auffälligkeit oder eigener Not-Aus).",
      "type": "https://splus-app.de/partner-api/fehler#app_gesperrt"
    },
    {
      "code": "partner_gesperrt",
      "status": 403,
      "beschreibung": "Der Partner ist gesperrt oder nicht (mehr) freigegeben.",
      "type": "https://splus-app.de/partner-api/fehler#partner_gesperrt"
    },
    {
      "code": "mandant_nicht_erlaubt",
      "status": 403,
      "beschreibung": "Dieser Mandant ist für die App nicht erreichbar (z. B. Sandbox-App gegen echten Mandanten, Live nicht freigegeben).",
      "type": "https://splus-app.de/partner-api/fehler#mandant_nicht_erlaubt"
    },
    {
      "code": "ip_nicht_erlaubt",
      "status": 403,
      "beschreibung": "Die Anfrage kommt von einer IP außerhalb der IP-Allowlist der App.",
      "type": "https://splus-app.de/partner-api/fehler#ip_nicht_erlaubt"
    },
    {
      "code": "nicht_gefunden",
      "status": 404,
      "beschreibung": "Der Datensatz existiert nicht oder ist für diesen Mandanten nicht sichtbar.",
      "type": "https://splus-app.de/partner-api/fehler#nicht_gefunden"
    },
    {
      "code": "validierung",
      "status": 422,
      "beschreibung": "Ungültige Parameter (z. B. limit außerhalb 1–100, aenderung_seit kein ISO-8601, ungültiger cursor).",
      "type": "https://splus-app.de/partner-api/fehler#validierung"
    },
    {
      "code": "rate_limit",
      "status": 429,
      "beschreibung": "Zu viele Anfragen pro Minute. Retry-After beachten.",
      "type": "https://splus-app.de/partner-api/fehler#rate_limit"
    },
    {
      "code": "kontingent_erschoepft",
      "status": 429,
      "beschreibung": "Das Monatskontingent des Tarifs ist aufgebraucht.",
      "type": "https://splus-app.de/partner-api/fehler#kontingent_erschoepft"
    },
    {
      "code": "mandant_gesperrt",
      "status": 503,
      "beschreibung": "Der Mandant ist vorübergehend gesperrt.",
      "type": "https://splus-app.de/partner-api/fehler#mandant_gesperrt"
    },
    {
      "code": "partner_api_aus",
      "status": 503,
      "beschreibung": "Die Partner-API ist plattformweit abgeschaltet (Not-Aus). Später erneut versuchen.",
      "type": "https://splus-app.de/partner-api/fehler#partner_api_aus"
    }
  ],
  "x-schreibende-scopes": [
    "termine.schreiben",
    "zeit.schreiben",
    "rapporte.schreiben"
  ],
  "x-grenzen": {
    "rate_limit": "Anfragen pro Minute je App und Mandant laut Tarif (Header X-RateLimit-*); bei Überschreitung 429 rate_limit mit Retry-After.",
    "kontingent": "Monatliches Aufrufkontingent laut Tarif; danach 429 kontingent_erschoepft bis Monatsende oder Tarifwechsel.",
    "drosselung": "Bei auffälliger Nutzung (z. B. massenhaftes Abrufen) kann der Betreiber eine App drosseln oder sperren."
  }
}
