Termine empfangen
Ein Termin erreicht Ihr System in drei Schritten. Der dritte darf ablehnen; eine Ablehnung ist ein vorgesehener Ausgang und kein Fehlerfall.
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.
Ihr System muss keine Slot-Suche implementieren. Deshalb genügt das Format busy.
2 · Reservieren Extended
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:
- Ihr System bestimmt die Haltedauer und gibt sie zurück. Petla nimmt keinen Wert an.
- Eine Reservierung muss nach Ablauf automatisch verfallen.
hold.releasemuss idempotent sein.- Eine Reservierung sollte sich in der Oberfläche der Praxis von einem bestätigten Termin unterscheiden lassen.
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:
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" }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": "…" }| Grund | Bedeutung |
|---|---|
SLOT_TAKEN | Die Zeit wurde zwischen Veröffentlichung und Buchung in Ihrem System vergeben. |
RESOURCE_UNAVAILABLE | Der genannte Behandler arbeitet dann nicht. |
CLIENT_BLOCKED | Die Praxis nimmt diesen Kunden nicht an. |
OUT_OF_HOURS | Außerhalb der tatsächlichen Öffnungszeiten. |
SLOT_OFF_GRID | Die Praxis ist offen und die Zeit ist frei — aber die Startzeit liegt nicht auf dem Raster, das Ihr System speichern kann (bookingGridMinutes). |
REJECTED_BY_PRACTICE | Ein Mensch hat abgelehnt. |
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.
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.
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.
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.
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.
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.
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 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" } ]
}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:
| Wert | Bedeutung |
|---|---|
answered | Die Form des Werts passt zum Fragetyp. |
unexpected-value | Ein 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. |
orphan | Eine 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-config | Die Praxis hat diese Frage inzwischen abgeschaltet — die Antwort ist älter. |
parent-not-affirmative | Ein bedingtes Feld, dessen übergeordnete Frage nicht mehr ja sagt. Das öffentliche Formular blendet es aus; die Antwort ist echt und gespeichert. |
option-not-in-template | Ein Auswahlwert, den die Vorlage heute nicht mehr anbietet. |
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.
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.
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 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.
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.
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.
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.
Sortieren Sie nach version, nie nach einem Zeitstempel.