Termine empfangen

Ein Termin erreicht Ihr System in drei Schritten. Der dritte darf ablehnen; eine Ablehnung ist ein vorgesehener Ausgang und kein Fehlerfall.

1 · AnbietenPetla berechnet mögliche Zeiten aus Ihrer Verfügbarkeit und den eigenen Buchungsregeln.
2 · ReservierenOptional. Petla hält den Slot, während der Halter das Formular ausfüllt.
3 · BuchenPetla schreibt den Termin. Ihr System prüft erneut und darf ablehnen.

1 · Anbieten Core

Petla berechnet die möglichen Zeiten selbst — aus der Verfügbarkeit, die Sie veröffentlicht haben, plus den eigenen Regeln: Dauer je Anliegen, Mindestvorlauf, Buchungshorizont, Mehrtier-Regeln, Fahrzeit bei Hausbesuchen.

Hinweis

Ihr System muss keine Slot-Suche implementieren. Deshalb genügt das Format busy.

2 · Reservieren Extended

Die einzige Stelle, an der Petla Sie aufruft

Die Reservierung ist deshalb Extended. Alle übrigen Aufrufe dieser Schnittstelle gehen von Ihrem System an Petla, sodass auch ein Praxisrechner hinter einem Router die vollständige Integration betreiben kann. Eine Reservierung funktioniert so nicht: Die Sperre muss sofort im Kalender der Praxis erscheinen, damit am Tresen nicht 10:20 vergeben wird, während ein Halter noch das Formular ausfüllt.

Wenn Ihr System keinen eingehenden HTTPS-Endpunkt bereitstellen kann, dürfen Sie appointments.hold nicht erklären.

Systeme, die das können, stellen zwei Operationen bereit — hold.create und hold.release:

Achtung

Ohne Reservierung bucht Petla direkt. Wird die Zeit in Ihrem System zwischenzeitlich vergeben, erfährt der Tierhalter das als fehlgeschlagene Buchung, nachdem er das Formular ausgefüllt hat. Die Integration bleibt funktionsfähig; die Erfahrung ist schlechter. Für Systeme, die vor Ort in der Praxis laufen, ist das der Normalfall.

3 · Buchen und bestätigen Core

Ihr System empfängt den Termin (über den Ereignis-Strom), schreibt ihn in den Kalender und bestätigt:

Der Termin ist bei Petla bereits bestätigt

Petla kennt keinen unbestätigten Zustand für Termine vor Ort und für Hausbesuche: Eine Buchung entsteht bereits bestätigt und steht sofort im Kalender der Praxis. Ihre Bestätigung ist deshalb keine Freigabe, sondern eine Rückmeldung.

Zurückgehalten wird nur die Bestätigungs-E-Mail an den Tierhalter, für die Dauer des Fensters. Ein accepted: false innerhalb des Fensters storniert den bestehenden Termin und löst statt der Bestätigung die Absage-Mail unten aus. Behandeln Sie eine Ablehnung also als Stornierung eines bestehenden Termins, nicht als Zurückweisung einer Anfrage.

POST /appointments/{appointmentId}/ack

{ "accepted": true, "externalId": "PMS-2026-0091" }
Achtung

Schicken Sie denselben Rumpf noch einmal — er antwortet wieder 200, mit Idempotent-Replayed: true im Kopf. Erst ein widersprechender Rumpf — anderer externalId, oder accepted: false, nachdem Sie zugesagt hatten — antwortet 409 already_acknowledged.

⚠️ Dieser Fall tritt regelmäßig auf: Ihr HTTP-Client wiederholt den Aufruf nach einem Timeout von selbst. Die Bestätigung nimmt deshalb keinen Idempotency-Key — der Rumpf selbst ist der Schlüssel, weil ein zweiter Versuch denselben Termin auf denselben Zustand setzt.

Ablehnen, mit maschinenlesbarem Grund

{ "accepted": false, "reason": "SLOT_TAKEN", "message": "…" }
GrundBedeutung
SLOT_TAKENDie Zeit wurde zwischen Veröffentlichung und Buchung in Ihrem System vergeben.
RESOURCE_UNAVAILABLEDer genannte Behandler arbeitet dann nicht.
CLIENT_BLOCKEDDie Praxis nimmt diesen Kunden nicht an.
OUT_OF_HOURSAußerhalb der tatsächlichen Öffnungszeiten.
SLOT_OFF_GRIDDie Praxis ist offen und die Zeit ist frei — aber die Startzeit liegt nicht auf dem Raster, das Ihr System speichern kann (bookingGridMinutes).
REJECTED_BY_PRACTICEEin Mensch hat abgelehnt.
Was Ihr Grund dem Tierhalter sagt

Petla fasst die sechs Gründe zu zwei Nachrichten an den Tierhalter zusammen. Ihr Grund steuert dabei nur, ob Petla neue Zeiten anbietet:

  • SLOT_TAKEN, RESOURCE_UNAVAILABLE, OUT_OF_HOURS, SLOT_OFF_GRID — die Zeit ist nicht mehr frei. Petla bietet erneut an, mit einem Link zurück auf die Buchungsseite der Praxis.
  • CLIENT_BLOCKED, REJECTED_BY_PRACTICE — die Praxis konnte diesen Termin nicht annehmen. Petla bietet nichts erneut an und verweist auf die Praxis.

Wählen Sie den Grund entsprechend: Die erste Gruppe lädt zum Neubuchen ein, die zweite würde den Halter in dieselbe Ablehnung zurückschicken. Keine der beiden Nachrichten spricht von einer Absage — der Termin hat den Kalender der Praxis nie erreicht.

SLOT_OFF_GRID entfällt mit bookingGridMinutes

Wenn Sie bookingGridMinutes deklarieren, rechnet Petla Ihr Raster bereits in die angebotenen Zeiten ein — dieser Grund kann dann gar nicht mehr auftreten. Schicken Sie ihn trotzdem, ist das ein nützliches Signal: Entweder ist das Raster nicht deklariert, oder es hat sich nach dem Angebot geändert, oder die beiden Angaben widersprechen sich.

Das Zeitfenster beträgt 60 Sekunden

Petla hält die Bestätigungs-E-Mail zurück, bis Ihre Rückmeldung da ist oder das Fenster abläuft. Der Tierhalter wartet so lange auf eine Antwort.

Danach bestätigt Petla dem Halter trotzdem — und eine später eintreffende Ablehnung wird zu einer Absage, die dem Halter mitgeteilt werden muss.

externalId ist die Kennung des Termins in Ihrem System. Petla speichert sie und nennt sie in jeder späteren Nachricht zu diesem Termin — auch im Support.

Kunden und Patienten zuordnen Extended

Petla schickt Halter und Tiere so mit, wie Petla sie führt. Ihr System entscheidet, ob das ein bestehender Kunde ist, und sagt es in der Bestätigung:

POST /appointments/{appointmentId}/ack

{
  "accepted": true,
  "externalId": "PMS-2026-0091",
  "clientMatch": { "outcome": "matched", "externalClientId": "K-4711" }
}

outcome ist matched, created oder ambiguous. externalClientId ist unter jedem Ergebnis optional — auch unter ambiguous, wo Sie sich per Definition nicht festgelegt haben.

Hinweis

clientMatch ist ein Feld in der Bestätigung, kein eigener Aufruf. Es gibt keinen Endpunkt für die Kundenzuordnung, und Petla berechnet das Ergebnis nicht: Ihr System gleicht mit Ihrer eigenen Kundendatei ab und meldet, was dabei herauskam. Petla speichert die Antwort, wie sie ankommt.

Achtung

ambiguous ist eine echte Antwort und muss benutzt werden. Es bedeutet: Ihr System hat mehr als einen plausiblen Kunden gefunden und sich nicht entschieden. Petla legt das der Praxis vor, statt eine Buchung an die falsche Akte zu hängen.

Petla führt vorher eine eigene Zuordnung durch — exakte E-Mail zuerst, dann Telefon (letzte 9 Ziffern) und Nachname plus Geburtsdatum — und schickt maskierte Beinahe-Treffer mit. Benutzen Sie client.petlaBookerId als stabilen Schlüssel über Buchungen hinweg, statt jedes Mal neu zuzuordnen.

Achtung

Ein Kunde ohne E-Mail-Adresse wird mit einer synthetischen Adresse unter einer reservierten Domain übergeben (…@petla.internal). Diese Adresse darf nicht angezeigt und niemals angeschrieben werden.

Der Anamnesebogen zum Termin Extended

Hat der Tierhalter vor dem Besuch den Anamnesebogen der Praxis ausgefüllt, liegt er dem Termin bei: forms[] trägt die Antworten maschinenlesbar, und GET /forms/{submissionId}/pdf liefert dieselbe Einreichung als das Dokument, das die Praxis liest und ablegt.

Erst deklarieren, dann lesen

Ohne forms.receive in PUT /capabilities fehlt forms auf jedem Termin, und das PDF antwortet mit 400 capability_not_declared. Ein Anamnesebogen enthält Angaben, deren unbefugte Offenbarung aus einer Tierarztpraxis unter § 203 Abs. 1 Nr. 1 StGB fällt; ohne erklärte Fähigkeit werden sie nicht übertragen.

Ohne die Fähigkeit ändert sich nichts an einer bestehenden Integration: Die Praxis liest den Bogen weiterhin in Petla, so wie sie es bisher getan hat.

Kein Schlüssel und ein leeres Array bedeuten das Gegenteil

Kein forms-Schlüssel heißt: Diese Verbindung hat forms.receive nicht deklariert. "forms": [] heißt: Sie haben deklariert, und zu diesem Termin liegt kein bestätigter Bogen vor.

Werden beide gleich behandelt, erscheint „wir senden Ihnen keine Bögen“ als „der Halter hat nichts ausgefüllt“, und zwar bei jedem Termin. Es gibt keinen Entwurfszustand: Ein abgebrochener Bogen hinterlässt gar nichts, ein leeres Array bedeutet also nie „noch in Arbeit“.

{
  "type": "ANAMNESIS",
  "submission": { "id": "3f7c…", "confirmedAt": "2026-09-04T08:30:00.000Z",
                  "formName": "Kardiologie Vorbericht",
                  "formTemplateVersion": 3, "locale": "de" },
  "pdfUrl": "…/api/partner/v1/forms/3f7c…/pdf",
  "pdfContentType": "application/pdf",
  "counts": { "answered": 7, "unexpected": 0, "orphan": 1, "unanswered": 12 },
  "entries": [ { "id": "kardio_med_pimobendan_dosis",
                 "path": ["kardio_medikation_aktuell", "kardio_med_pimobendan"],
                 "type": "text", "label": "Dosierung",
                 "value": "5 mg, zweimal täglich",
                 "display": "5 mg, zweimal täglich",
                 "status": "answered", "flags": [],
                 "category": "Herz und Kreislauf" } ],
  "discussInPractice": [ { "id": "kardio_nebenwirkungen",
                           "path": ["kardio_medikation_aktuell"],
                           "type": "text", "label": "Nebenwirkungen",
                           "category": "Herz und Kreislauf" } ]
}
Übernehmen Sie value, nie display

value ist die roh gespeicherte Antwort und bewusst nicht typisiert — ein Gewicht kommt als { "unit": "kg", "value": 28.4 }, eine Mehrfachauswahl als Liste. display ist die Zeile, die im PDF steht. Sie erlaubt einen Sichtvergleich zwischen dem Dokument des Tierarztes und Ihrem Import, und sie ändert sich mit der Darstellung.

status und flags machen kenntlich, wenn ein Wert nicht zum Fragetyp passt oder die Vorlage ihn nicht mehr kennt. Beides sind Verzweigungen, keine Filter:

WertBedeutung
answeredDie Form des Werts passt zum Fragetyp.
unexpected-valueEin Wert liegt vor, in einer Form, die dieser Fragetyp nicht speichert. Er behält seinen Rohwert, statt umgedeutet zu werden: unter einer Ja/Nein-Frage würde ein unbekannter Wert sonst als Nein gedruckt.
orphanEine Antwort, deren Frage es in der Vorlage nicht mehr gibt. type ist "unknown", category ist null. Sie wird mitgeschickt: Der Halter hat sie gegeben, und ohne sie würden gedrucktes und importiertes Protokoll voneinander abweichen.
disabled-in-clinic-configDie Praxis hat diese Frage inzwischen abgeschaltet — die Antwort ist älter.
parent-not-affirmativeEin bedingtes Feld, dessen übergeordnete Frage nicht mehr ja sagt. Das öffentliche Formular blendet es aus; die Antwort ist echt und gespeichert.
option-not-in-templateEin Auswahlwert, den die Vorlage heute nicht mehr anbietet.
Achtung

entries[] wird gegen den heutigen Fragebaum der Vorlage gebaut, während formTemplateVersion die Fassung nennt, die der Halter beantwortet hat. Diese Lücke drückt orphan aus. counts.unanswered ist die Gegenseite: Fragen der Vorlage ohne jeden Wert, gezählt und nicht aufgeführt, damit die vier Zahlen — zusammen mit der Länge von discussInPractice — den ganzen Bogen abdecken.

locale ist die Sprache der Praxis, nicht die des Halters: Das Dokument liest der Tierarzt, und der Halter hatte das Formular beim Ausfüllen ohnehin in seiner eigenen Sprache.

discussInPractice ist keine Antwort

Seit 2026-10-23. Im Bogen kann der Halter statt einer Antwort „Bespreche ich lieber in der Praxis“ wählen. Jede so markierte Frage steht in discussInPractice[] — mit id, path, type, label und category wie ein Eintrag, aber ohne value. Im PDF steht an ihrer Stelle „Möchte das in der Praxis besprechen“. Das Array ist immer da und leer, wenn nichts markiert wurde.

Übernehmen Sie es als Hinweis für das Gespräch, nie als Nein, keine bekannt oder leere Antwort: „Fragen Sie mich persönlich“ ist das Gegenteil von „nein“. Eine markierte Frage steht nie in entries[] und zählt weder als answered noch als unanswered. Hat der Halter sie trotzdem beantwortet, gilt die Antwort, und die Markierung entfällt.

Kein Halter-Block im Bogen

Wer der Tierhalter ist, steht bereits in client und patients[] desselben Termins. Eine zweite Kopie im Bogen führte dieselbe E-Mail-Adresse zweimal im selben Rumpf, ohne dass erkennbar wäre, welche die aktuelle ist. Die beiden Stellen beantworten außerdem Verschiedenes: Der Bogen hält fest, was ins Formular getippt wurde; die Buchung, wer die Person ist.

Das PDF — mit Ihrem gewöhnlichen Token

GET /forms/{submissionId}/pdf
Authorization: Bearer <Ihr Verbindungs-Token>
Petla-Clinic-Id: <clinicId>
pdfUrl ist nicht signiert

pdfUrl im Rumpf ist genau dieser Pfad. Sie rufen ihn mit demselben Verbindungs-Token auf, mit dem Sie den Termin gelesen haben; im Browser eingefügt liefert er einen 401. Eine URL in einem Rumpf ist üblicherweise vorab authentifiziert; diese hier nicht.

Gegenüber einem signierten Link hat das drei Eigenschaften:

  • Nichts überdauert den Widerruf. Ein signierter Link liefert Patientenangaben weiter aus, nachdem die Praxis die Verbindung beendet hat; dieser Pfad antwortet beim nächsten Aufruf mit 401.
  • Es gibt kein zweites Geheimnis, das erzeugt, gespeichert, rotiert oder verloren werden kann.
  • Es gilt dieselbe Authentifizierung wie bei allen anderen Aufrufen.
Achtung

Eine Einreichung einer anderen Praxis und eine, die es nicht gibt, sind derselbe 404 — sonst beantwortet dieser Pfad die Frage „gibt es diese Kennung?“ für jeden, der irgendein Token hält.

Es gibt kein POST. Der Bogen kommt vom Halter, und der Speicher ist nur anfügend: Die Einreichung lässt sich nachträglich nicht ändern.

Änderungen aus Ihrem System Extended

Eine Verschiebung oder Absage, die am Tresen passiert, muss gemeldet werden: POST /appointments/{appointmentId}/reschedule beziehungsweise /cancel.

Erst deklarieren, dann aufrufen

Was diese beiden Endpunkte schützt, ist nicht die Auslieferung, sondern die Fähigkeit: Ohne appointments.rescheduleInbound beziehungsweise appointments.cancelInbound in PUT /capabilities antworten sie mit 400 capability_not_declared.

Die Gegenrichtung ist davon unabhängig: Eine in Petla verschobene oder abgesagte Buchung erreicht Sie als appointment.rescheduled beziehungsweise appointment.cancelled über den Ereignis-Strom — auch dann, wenn Sie diese beiden Endpunkte nie aufrufen. Das ist Core.

Achtung

Ohne diese Meldung beschreiben Petlas Erinnerungen und die App des Tierhalters weiterhin einen Termin, den es nicht mehr gibt.

Die Versionsnummer je Termin

Jeder Termin trägt version, eine je Termin monoton steigende Ganzzahl. Sie entscheidet über Reihenfolge, Dopplung und Veralten: Haben Sie Version 3 bereits angewendet, ignorieren Sie Version 2, wenn sie verspätet eintrifft.

Achtung

Sortieren Sie nach version, nie nach einem Zeitstempel.