> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rxscale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduling

> Patienten können Terminarten anzeigen, freie Slots buchen und Videosprechstunden starten

# Scheduling

Die Scheduling API ermöglicht patientenseitige Selbstbuchung für Videosprechstunden. Öffentliche Discovery-Endpunkte können mit einer `shop_uid` aufgerufen werden. Buchungs- und Umbuchungsvorgänge nutzen entweder einen Organisations-API-Key oder einen kurzlebigen Patient Booking Token.

<Warning>
  Übermitteln Sie signierte Patient Booking Tokens niemals in URLs. Tauschen Sie das Token gegen einen `booking_launch_code` ein und verwenden Sie diesen Code für gehostete Buchungslinks oder eingebettete Widgets.
</Warning>

## Authentifizierung

Die Scheduling API akzeptiert zwei Credentials. Die meisten Endpunkte akzeptieren beides; einige sind auf eines beschränkt.

### Patient Booking Tokens (empfohlen für Patientenflüsse)

Ein kurzlebiges JWT, das Ihr Partner-Backend für einen bestimmten Patienten ausstellt. Übergeben Sie es im Header `X-RxScale-Booking-Token`. Das Token wird gegen ein in RxScale hinterlegtes Organisations-Booking-Token-Secret verifiziert.

**Algorithmus:** `HS256`

**JOSE-Header:** muss `kid` enthalten — die `key_id`, die beim Provisionieren des Secrets zurückgegeben wurde.

**Erforderliche Claims:**

| Claim                    | Typ    | Beschreibung                                                                                                                |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------- |
| `aud`                    | string | Muss `rxscale-scheduling` sein.                                                                                             |
| `exp`                    | int    | Ablauf (Unix-Sekunden). Kurz halten — unter 5 Minuten ist üblich.                                                           |
| `nbf`                    | int    | Not-before (Unix-Sekunden).                                                                                                 |
| `iat`                    | int    | Issued-at (Unix-Sekunden).                                                                                                  |
| `shop_identifier`        | string | Der in RxScale konfigurierte Shop-Identifier.                                                                               |
| `shop_customer_id`       | string | Ihre stabile Kunden-ID. RxScale dedupliziert Patienten anhand dieser ID.                                                    |
| `patient_profile_fields` | array  | Profilfelder, die übernommen werden sollen. Jeder Eintrag: `{ "field": "<field_key>", "value": <wert>, "source": "shop" }`. |

```python theme={null}
import jwt, time
now = int(time.time())
payload = {
    "aud": "rxscale-scheduling",
    "exp": now + 300,
    "nbf": now - 10,
    "iat": now,
    "shop_identifier": "shop_abc",
    "shop_customer_id": "cust_123",
    "patient_profile_fields": [
        {"field": "first_name", "value": "Ada", "source": "shop"},
        {"field": "last_name",  "value": "Lovelace", "source": "shop"},
        {"field": "email",      "value": "ada@example.com", "source": "shop"},
    ],
}
token = jwt.encode(payload, SECRET, algorithm="HS256", headers={"kid": KEY_ID})
```

<Note>
  Booking-Token-Secrets werden in der RxScale-Datenbank **verschlüsselt gespeichert**. Nur Ihre `key_id` liegt im Klartext für das Routing vor; der Secret-Wert wird sowohl bei der Token-Verifikation als auch beim Abruf durch einen Admin (Admin-Portal oder Secrets-API) im Speicher entschlüsselt. Rotieren Sie, indem Sie ein neues Secret provisionieren und das alte widerrufen, sobald Ihr Minter umgestellt ist.
</Note>

### Organisations-API-Key

Senden Sie `X-API-Key: <key>` statt des Booking Tokens für Server-zu-Server-Flüsse. Der Key benötigt die Berechtigung `scheduling:write`. Nutzen Sie das für Back-Office-Tools, skriptgesteuertes Umbuchen oder Operations-Skripte.

## Terminarten auflisten

```bash theme={null}
GET /v1/scheduling/appointment-types?shop_uid={shop_uid}
```

<ParamField query="shop_uid" type="string" required>
  Shop-UID, mit der die Terminarten auf die richtige Organisation eingeschränkt werden.
</ParamField>

Kein API-Key erforderlich.

```bash theme={null}
curl "https://api.rxscale.com/v1/scheduling/appointment-types?shop_uid=shop_123"
```

```json theme={null}
{
  "data": [
    {
      "uid": "apt_video_15",
      "name": "Videosprechstunde",
      "duration_minutes": 15,
      "hold_ttl_seconds": 900,
      "booking_min_notice_minutes": 10,
      "doctor_assignment_mode": "all_available_doctors",
      "allow_patient_rebooking": true,
      "rebooking_mode": "same_doctor_only"
    }
  ]
}
```

## Slots suchen

```bash theme={null}
POST /v1/scheduling/slots/search
```

Kein API-Key erforderlich.

Die Slot-Suche liefert nur Slots zurück, die den wirksamen Buchungsvorlauf der
Terminart erfüllen. Wenn ein Arzt für diese Terminart eine eigene Einstellung
hat, geht dieser Wert vor, auch `0` Minuten. Terminarten mit
`selected_doctors_only` liefern nur Ärzte mit aktiver arztspezifischer
Einstellung zurück.

<ParamField body="shop_uid" type="string" required />

<ParamField body="appointment_type_uid" type="string" required />

<ParamField body="from" type="int" required>Fensterstart (Unix-Sekunden).</ParamField>
<ParamField body="to" type="int" required>Fensterende (Unix-Sekunden). Max. 31 Tage ab `from`.</ParamField>
<ParamField body="doctor_uid" type="string">Auf einen Arzt einschränken.</ParamField>
<ParamField body="timezone" type="string">IANA-Zeitzone (z. B. `Europe/Berlin`) zur Lokalisierung der Ausgabe. Standard ist `Europe/Berlin`. Jeder Slot enthält `start_local`/`end_local` in dieser Zone; `start_date`/`end_date` bleiben Unix-Sekunden.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/slots/search" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_uid": "shop_123",
    "appointment_type_uid": "apt_video_15",
    "from": 1778457600,
    "to": 1778544000
  }'
```

```json theme={null}
{
  "slots": [
    {
      "doctor_uid": "doc_123",
      "doctor_name": "Dr. Max Meyer",
      "start_date": 1778490000,
      "end_date": 1778490900,
      "timezone": "Europe/Berlin",
      "start_local": "2026-05-11T11:00:00+02:00",
      "end_local": "2026-05-11T11:15:00+02:00"
    }
  ]
}
```

## Booking Session erstellen

Verwenden Sie diesen Endpunkt beim Start der gehosteten UI oder des Widget-Skripts. Die Anfrage kann das signierte Patient Booking Token entweder in `X-RxScale-Booking-Token` oder im JSON-Body als `booking_token` enthalten.

```bash theme={null}
POST /v1/scheduling/booking-sessions
```

<ParamField body="mode" type="string" required>`booking` für einen neuen Termin, `rebooking` für eine bestehende Buchung.</ParamField>
<ParamField body="appointment_type_uid" type="string">Pflicht im `booking`-Modus.</ParamField>
<ParamField body="from" type="int">Fensterstart (Unix-Sekunden). Pflicht im `booking`-Modus.</ParamField>
<ParamField body="to" type="int">Fensterende (Unix-Sekunden). Pflicht im `booking`-Modus.</ParamField>
<ParamField body="return_url" type="string">URL, zu der der Patient nach erfolgreicher Buchung weitergeleitet wird.</ParamField>
<ParamField body="booking_token" type="string">Patient Booking JWT, falls nicht im Header übergeben.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/booking-sessions" \
  -H "Content-Type: application/json" \
  -H "X-RxScale-Booking-Token: eyJ..." \
  -d '{
    "mode": "booking",
    "appointment_type_uid": "apt_video_15",
    "from": 1778457600,
    "to": 1778544000,
    "return_url": "https://partner.example/booking-complete"
  }'
```

```json theme={null}
{
  "booking_launch_code": "blc_Nc8...",
  "expires_at": 1778458500,
  "launch_url": "https://meetings.rxscale.com/booking?launch=blc_Nc8..."
}
```

## Booking Session abrufen

Die gehostete UI ruft diesen Endpunkt nur mit dem Launch-Code auf, um den Buchungskontext zu laden. Kein Auth-Header nötig — der Launch-Code selbst ist das Credential.

```bash theme={null}
GET /v1/scheduling/booking-sessions/{booking_launch_code}
```

```bash theme={null}
curl "https://api.rxscale.com/v1/scheduling/booking-sessions/blc_Nc8..."
```

```json theme={null}
{
  "uid": "bsn_abc...",
  "shop_uid": "shop_123",
  "appointment_type_uid": "apt_video_15",
  "appointment_type": {
    "uid": "apt_video_15",
    "name": "Videosprechstunde",
    "duration_minutes": 15
  },
  "start_from": 1778457600,
  "start_to": 1778544000,
  "mode": "booking",
  "return_url": "https://partner.example/booking-complete",
  "expires_at": 1778458500
}
```

Die Antwort enthält bewusst kein `patient_profile_uid` und keine weiteren Patienten-Identifier, damit der Launch-Code gefahrlos durch die URL im Browser des Patienten geführt werden kann.

## Holds erstellen und bestätigen

Authentifizierte Integrationen können Holds mit einem API-Key mit `scheduling:write` anlegen. Gehostete-UI-Integrationen nutzen die `booking_launch_code`-Routen.

```bash theme={null}
POST /v1/scheduling/booking-sessions/{booking_launch_code}/holds
```

<ParamField body="doctor_uid" type="string" required />

<ParamField body="appointment_type_uid" type="string" required />

<ParamField body="start_date" type="int" required>Unix-Sekunden.</ParamField>
<ParamField body="visit_reason" type="string">Optionaler Grund für den Termin, bis zu 2000 Zeichen. Leere Werte werden als `null` gespeichert.</ParamField>
<ParamField header="Idempotency-Key" type="string">Optional. Derselbe Key mit identischem Body liefert den bestehenden Hold zurück, statt einen neuen anzulegen.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/booking-sessions/blc_Nc8.../holds" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: hold-attempt-1" \
  -d '{
    "doctor_uid": "doc_123",
    "appointment_type_uid": "apt_video_15",
    "start_date": 1778490000,
    "visit_reason": "Medikationsreview vor Dosisanpassung"
  }'
```

```json theme={null}
{
  "hold_uid": "pdm_123",
  "expires_at": 1778490900,
  "ttl_seconds": 900,
  "status": "held",
  "visit_reason": "Medikationsreview vor Dosisanpassung"
}
```

```bash theme={null}
POST /v1/scheduling/booking-sessions/{booking_launch_code}/holds/{hold_uid}/confirm
```

<ParamField body="visit_reason" type="string">Optionaler Grund für den Termin, bis zu 2000 Zeichen. Wird dieses Feld weggelassen, bleibt der beim Erstellen des Holds gesetzte Wert erhalten; ein leerer Wert löscht ihn.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/booking-sessions/blc_Nc8.../holds/pdm_123/confirm" \
  -H "Content-Type: application/json" \
  -d '{
    "visit_reason": "Medikationsreview vor Dosisanpassung"
  }'
```

```json theme={null}
{
  "uid": "pdm_123",
  "status": "confirmed",
  "start_date": 1778490000,
  "end_date": 1778490900,
  "visit_reason": "Medikationsreview vor Dosisanpassung"
}
```

<Note>
  Wenn ein Hold abgelaufen ist, der Slot aber noch nicht durch einen anderen Termin belegt wurde, ist die Bestätigung weiterhin möglich und der Termin wechselt in den Status `confirmed`.
</Note>

<Note>
  Wenn `visit_reason` gesetzt ist, ist der Grund für Ärzte/Admins sichtbar und kann in Termin-Erinnerungen per E-Mail/SMS sowie in synchronisierten Kalendereinladungen erscheinen. Er kann beim Erstellen des Holds gesetzt und beim Bestätigen bearbeitet oder gelöscht werden — ein Request-Body ohne `visit_reason` lässt den vorhandenen Wert unverändert.
</Note>

## Termin stornieren

Storniert einen bestätigten oder gehaltenen Termin. Akzeptiert entweder ein Booking Token (patientenseitige Stornierung) oder einen Organisations-API-Key (Operations).

```bash theme={null}
POST /v1/scheduling/appointments/{appointment_uid}/cancel
```

<ParamField body="reason" type="string">Optionaler Freitext-Grund. Wird am Termin für Audit-Zwecke gespeichert.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointments/pdm_123/cancel" \
  -H "X-RxScale-Booking-Token: eyJ..." \
  -H "Content-Type: application/json" \
  -d '{"reason": "patient_request"}'
```

```json theme={null}
{
  "appointment_uid": "pdm_123",
  "status": "cancelled"
}
```

Patientenseitige Stornierungen respektieren `cancellation_min_notice_minutes` der Terminart. Eine Stornierung innerhalb der Frist gibt `400` mit dem betroffenen Feld zurück; buchen Sie den Patienten um oder rufen Sie den Endpunkt mit einem API-Key auf, wenn ein operativer Override nötig ist.

## Termin umbuchen

Verschiebt einen bestehenden Termin auf einen neuen Slot. Der Rebook-Flow legt atomar einen neuen Termin an (mit `previous_meeting_uid` auf den Originaltermin) und storniert den alten.

```bash theme={null}
POST /v1/scheduling/appointments/{appointment_uid}/rebook
```

<ParamField body="new_start_date" type="int" required>Unix-Sekunden für den neuen Slot.</ParamField>
<ParamField body="new_doctor_uid" type="string">Pflicht, wenn die Terminart `rebooking_mode: any_doctor` setzt. Bei `same_doctor_only` wird der ursprüngliche Arzt übernommen.</ParamField>
<ParamField body="appointment_type_uid" type="string">Optional. Standard ist die ursprüngliche Terminart.</ParamField>
<ParamField header="Idempotency-Key" type="string">Dringend empfohlen — schützt vor doppelten Umbuchungen bei Retries.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointments/pdm_123/rebook" \
  -H "X-RxScale-Booking-Token: eyJ..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rebook-attempt-1" \
  -d '{"new_start_date": 1778510000}'
```

```json theme={null}
{
  "appointment_uid": "pdm_456",
  "status": "confirmed"
}
```

Umbuchungen werden blockiert, wenn die Terminart `allow_patient_rebooking: false` setzt, `rebooking_min_notice_minutes` nicht eingehalten ist oder der neue Slot bereits belegt ist.

## Join-Token abrufen

Liefert ein kurzlebiges Jitsi-Waiting-Room-JWT und den Raumnamen. Wird von der Patient-UI verwendet, um den Videocall zu starten.

```bash theme={null}
POST /v1/scheduling/appointments/{appointment_uid}/join
```

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointments/pdm_123/join" \
  -H "X-RxScale-Booking-Token: eyJ..."
```

```json theme={null}
{
  "room_name": "doctor-123-room",
  "token": "eyJhbGciOi...",
  "expires_at": 1778497200
}
```

### Patienten-Meeting-Link zusammensetzen

Die Antwort enthält **keinen** fertigen Link. Ihre Integration muss ihn selbst zusammenbauen:

```
https://<meetings-app>/meeting/{room_name}?jwt={token}
```

Optional können Sie einen `return_url`-Query-Parameter anhängen, um den Patienten nach dem
Gesprächsende zurück auf Ihre Plattform weiterzuleiten:

```
https://<meetings-app>/meeting/{room_name}?jwt={token}&return_url=https%3A%2F%2Fpartner.example%2Fnach-dem-call
```

<ParamField query="jwt" type="string" required>
  Der `token`-Wert aus der Antwort dieses Endpunkts. Von RxScale signiert; nicht verändern.
</ParamField>

<ParamField query="return_url" type="string">
  URL, zu der der Patient nach dem Gesprächsende weitergeleitet wird. Die Weiterleitung erfolgt
  **nur**, wenn der Origin der URL (Schema + Host + Port) auf der admin-konfigurierten
  Allowlist der Organisation steht. Ist der Origin nicht auf der Liste oder wird kein
  `return_url` übergeben, kehrt der Patient einfach zur vorherigen Seite zurück. Konfigurieren
  Sie die Allowlist im Admin-Portal unter **Einstellungen → Rückleitungs-URLs**.
</ParamField>

<Note>
  Dieses `return_url` dient ausschließlich der Navigation nach dem Call auf der Meeting-Seite.
  Es ist vom `return_url` im Booking-Session-Body zu unterscheiden, das den Patienten nach
  einer erfolgreichen Buchung weiterleitet. Verwechseln Sie beide nicht.
</Note>

### Join-Fenster

Der Endpunkt akzeptiert Join-Anfragen ab **10 Minuten vor** `start_date` des Termins bis **60 Minuten nach** `end_date`. Anfragen außerhalb dieses Fensters liefern:

```json theme={null}
{ "error": ["Appointment is outside the join window"] }
```

Stellen Sie das in Ihrer UI klar dar — der Patient sollte einen Countdown oder einen "Join öffnet in N Minuten"-Hinweis sehen statt eines blockierten Buttons.

## Gehostete UI

Leiten Sie den Patienten auf die `launch_url` aus der Booking-Session-Antwort weiter oder binden Sie das Widget-Skript ein:

```html theme={null}
<script
  src="https://meetings.rxscale.com/booking-widget.js"
  data-launch-code="blc_Nc8..."
  data-target="#rxscale-booking"
  data-base-url="https://meetings.rxscale.com">
</script>
```

Das Widget rendert die gehostete Buchungsseite in einem iframe innerhalb eines Shadow-DOM-Wrappers, damit Styles nicht in die Host-Seite überschwappen.

## Aktions-Links in Terminerinnerungen

<Note>
  Diese Endpunkte werden von der RxScale-gehosteten Meetings-UI aufgerufen, wenn ein Patient einen
  **Beitreten**-, **Umbuchen**- oder **Stornieren**-Link aus einer Termin-Erinnerung per E-Mail
  oder SMS öffnet. Partner rufen sie nicht direkt auf — sie sind hier der Vollständigkeit halber
  dokumentiert.
</Note>

Jeder Erinnerungs-Link enthält einen von RxScale beim Versand der Erinnerung erzeugten,
undurchsichtigen Einmal-Launch-`code`. Der Code selbst ist das Credential — weder ein API-Key
noch ein Patient Booking Token wird auf diesen Routen benötigt oder akzeptiert.

### Aktions-Link-Kontext abrufen

Lädt den Anzeigekontext, den die gehostete UI benötigt, um die Landing-Page der Erinnerung zu
rendern.

```bash theme={null}
GET /v1/scheduling/appointment-actions/{code}
```

Kein Auth-Header nötig — der Code selbst ist das Credential. Ein fehlender, abgelaufener oder
nicht zugeordneter Code liefert `404`.

```bash theme={null}
curl "https://api.rxscale.com/v1/scheduling/appointment-actions/aac_Xy9..."
```

```json theme={null}
{
  "action": "rebook",
  "appointment_start": 1778490000,
  "appointment_end": 1778490900,
  "doctor_display_name": "Dr. Max Meyer",
  "appointment_type_name": "Videosprechstunde",
  "join_window": {
    "state": "too_early",
    "opens_at": 1778489400
  },
  "rebook_allowed": true,
  "cancel_allowed": true,
  "already_actioned": false,
  "doctor_uid": "doc_123",
  "rebooking_mode": "same_doctor_only"
}
```

| Feld                    | Typ     | Beschreibung                                                                                                                                                             |
| ----------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `action`                | string  | Welche Aktion dieser Link startet: `join`, `rebook` oder `cancel`                                                                                                        |
| `appointment_start`     | integer | Startzeit des Termins (Unix-Sekunden)                                                                                                                                    |
| `appointment_end`       | integer | Endzeit des Termins (Unix-Sekunden)                                                                                                                                      |
| `doctor_display_name`   | string  | Anzeigename des Arztes, für die Kopfzeile der Landing-Page                                                                                                               |
| `appointment_type_name` | string  | Name der Terminart                                                                                                                                                       |
| `join_window`           | object  | `{ state, opens_at }`. `state` ist `too_early`, `open` oder `too_late`; `opens_at` ist der Unix-Zeitstempel, zu dem der Raum öffnet (10 Minuten vor `appointment_start`) |
| `rebook_allowed`        | boolean | Ob ein Umbuchen für diesen Termin aktuell erlaubt ist                                                                                                                    |
| `cancel_allowed`        | boolean | Ob ein Stornieren für diesen Termin aktuell erlaubt ist                                                                                                                  |
| `already_actioned`      | boolean | Ob die Aktion dieses Links bereits abgeschlossen wurde (z. B. wurde der Termin darüber bereits storniert)                                                                |
| `doctor_uid`            | string  | Nur vorhanden, wenn `action` gleich `rebook` ist. Die UID des aktuellen Arztes                                                                                           |
| `rebooking_mode`        | string  | Nur vorhanden, wenn `action` gleich `rebook` ist. Entweder `same_doctor_only` oder `any_doctor_same_organisation`                                                        |

### Beitreten

Liefert dieselbe Waiting-Room-Token-Struktur wie [Join-Token abrufen](#join-token-abrufen).
Wiederverwendbar, solange das Join-Fenster offen bleibt.

```bash theme={null}
POST /v1/scheduling/appointment-actions/{code}/join
```

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointment-actions/aac_Xy9.../join"
```

```json theme={null}
{
  "room_name": "doctor-123-room",
  "token": "eyJhbGciOi...",
  "expires_at": 1778497200
}
```

Liefert `400`, wenn der Aufruf außerhalb des Join-Fensters erfolgt (10 Minuten vor
`appointment_start` bis 60 Minuten nach `appointment_end`).

### Stornieren

```bash theme={null}
POST /v1/scheduling/appointment-actions/{code}/cancel
```

<ParamField body="reason" type="string">Optionaler Freitext-Grund.</ParamField>

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointment-actions/aac_Xy9.../cancel" \
  -H "Content-Type: application/json" \
  -d '{"reason": "patient_request"}'
```

```json theme={null}
{
  "appointment_uid": "pdm_123",
  "status": "cancelled"
}
```

Einmalig: Hat der Code den Termin bereits storniert, liefert ein erneuter Aufruf `409` — ebenso,
wenn der Termin aus einem anderen Grund nicht mehr storniert werden kann (bereits
abgeschlossen oder außerhalb der Stornierungsfrist). Dies ist der einzige der vier Endpunkte,
der ein `patient_doctor_meeting_updated`-Event mit `change: "cancelled"` auslöst.

### Umbuchungs-Session starten

Legt eine gehostete Booking-Session im `rebook`-Modus an, damit der Patient über denselben
gehosteten Buchungsflow einen neuen Slot wählen kann, der unter
[Booking Session erstellen](#booking-session-erstellen) beschrieben ist.

```bash theme={null}
POST /v1/scheduling/appointment-actions/{code}/rebook-session
```

```bash theme={null}
curl -X POST "https://api.rxscale.com/v1/scheduling/appointment-actions/aac_Xy9.../rebook-session"
```

```json theme={null}
{
  "booking_launch_code": "blc_Rk2...",
  "expires_at": 1778458500,
  "launch_url": "https://meetings.rxscale.com/booking?launch=blc_Rk2..."
}
```

Idempotent — ein erneuter Aufruf vor Ablauf der Session liefert denselben
`booking_launch_code` zurück, statt einen neuen zu erzeugen, und verbraucht dabei nicht den
Aktions-Link-Code der Erinnerung. Der ursprüngliche Termin bleibt aktiv, bis der Patient
tatsächlich einen neuen Slot bestätigt: erst dann wird er storniert und ersetzt, wobei ein
`rebooked`-Event ausgelöst wird.

## Arztportal-Endpunkte

Diese Endpunkte sind auf den authentifizierten Arzt (Auth0-Token) eingeschränkt und werden vom
RxScale-Arztportal genutzt. Partner müssen sie in der Regel nicht direkt aufrufen.

### Geplante Termine auflisten

```bash theme={null}
GET /v1/doctor/appointments
```

Query-Parameter wie beim Admin-Listing: `status`, `from`, `to`, `patient_uid`, `page`, `limit`.
Die Antwort enthält ausschließlich Termine des authentifizierten Arztes.

### Verfügbarkeits-CRUD

```bash theme={null}
GET    /v1/doctor/availability-rules
POST   /v1/doctor/availability-rules
PATCH  /v1/doctor/availability-rules/{rule_uid}
DELETE /v1/doctor/availability-rules/{rule_uid}
```

Jede Regel hat `weekday` (0 = Montag … 6 = Sonntag), `start_time` und `end_time` in Minuten seit
Mitternacht, optional `buffer_minutes` zwischen Slots sowie optionale Gültigkeitsgrenzen
`valid_from` / `valid_until` (Unix-Sekunden). PATCH-Bodies sind partiell; mit
`clear_valid_from: true` oder `clear_valid_until: true` lassen sich gesetzte Grenzen entfernen.
DELETE ist ein Soft-Delete.

## Admin-Endpunkte

Auf die authentifizierte Admin-Organisation (Auth0-Token) eingeschränkt.

### Termine auflisten

```bash theme={null}
GET /v1/admin/scheduling/appointments
```

Query: `doctor_uid`, `patient_uid`, `status`, `from`, `to`, `page`, `limit`. Standard:
aktive (gehalten + bestätigt) Termine ab jetzt; mit `status=all` und `from=0` lässt sich die
Historie einsehen.

### Termin stornieren

```bash theme={null}
POST /v1/admin/scheduling/appointments/{appointment_uid}/cancel
```

Body `{"reason": "..."}` erforderlich.

### Termin­art-Erinnerungen

```bash theme={null}
GET    /v1/admin/scheduling/appointment-types/{appointment_type_uid}/reminders
POST   /v1/admin/scheduling/appointment-types/{appointment_type_uid}/reminders
PATCH  /v1/admin/scheduling/appointment-types/{appointment_type_uid}/reminders/{reminder_uid}
DELETE /v1/admin/scheduling/appointment-types/{appointment_type_uid}/reminders/{reminder_uid}
```

Jede Erinnerung hat `recipient_role` (`patient` / `doctor` / `admin`), `minutes_before` (positive
ganze Zahl bis 60 Tage), `send_email`, `send_sms` und `active`. Mehrere Erinnerungen pro
`(appointment_type, recipient_role)` sind erlaubt, solange sich der `minutes_before`-Offset
unterscheidet.

Ein Maintenance-Job läuft jede Minute, findet bestätigte Termine, deren Auslösefenster "jetzt"
umfasst, und publiziert pro auflösbarem Empfänger ein `scheduling.appointment_reminder_due`-Event.
Das Event feuert immer (Partner-Webhook-Subscriber erhalten es); der RxScale-eigene
Notification-Handler verschickt E-Mail/SMS nur, wenn die jeweiligen Flags `send_email` /
`send_sms` auf der Erinnerungszeile gesetzt sind.

Wenn der Termin einen `visit_reason` hat, enthalten das Reminder-Event und die RxScale-E-Mail/SMS-Inhalte diesen Grund.

Partner abonnieren das Event über den bestehenden Endpunkt für Organisations-
Benachrichtigungs-Abonnements mit `notification_type=APPOINTMENT_REMINDER_DUE`.

### Verfügbarkeiten eines Arztes ansehen

```bash theme={null}
GET /v1/admin/scheduling/doctors/{doctor_uid}/availability-rules
```

Lesende Auflistung der wöchentlichen Buchungsfenster eines Arztes. Gibt `404` zurück, wenn der
Arzt nicht in der Organisation des Admins ist.

### Booking-Token-Secrets

```bash theme={null}
GET    /v1/admin/scheduling/booking-token-secrets
GET    /v1/admin/scheduling/booking-token-secrets/{key_id}
POST   /v1/admin/scheduling/booking-token-secrets
DELETE /v1/admin/scheduling/booking-token-secrets/{key_id}
```

`POST` legt ein neues Secret an und gibt `{key_id, secret}` zurück. `GET` liefert den Wert
entschlüsselt zurück — Secrets liegen verschlüsselt in der Datenbank, sind aber von Admins
jederzeit über die API und das Admin-Portal (Einstellungen → Buchungs-Secrets) wieder lesbar,
damit ein neuer Minter ohne Neu-Provisionierung konfiguriert werden kann. `DELETE` widerruft
das Secret.

Die legacy organisations-gebundenen Pfade
(`/v1/admin/scheduling/organisations/{organisation_uid}/booking-token-secrets[/<key_id>]`)
werden weiterhin akzeptiert und gegen die authentifizierte Organisation validiert.

## Fehler

Die Scheduling API nutzt Standard-HTTP-Statuscodes:

| Status | Bedeutung                                                                                                                                                                   |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Schema-Validierung fehlgeschlagen oder eine Geschäftsregel hat die Aktion blockiert (Details im `error`-Body).                                                              |
| `401`  | Booking Token oder API-Key fehlt oder ist ungültig.                                                                                                                         |
| `404`  | Ressource nicht gefunden **oder** der Aufrufer ist nicht berechtigt, sie zu sehen. Bei unberechtigtem Zugriff liefert die API `404`, um Resource-Enumeration zu verhindern. |
| `409`  | Slot wurde zwischen Hold und Confirm anderweitig belegt. Neu suchen und erneut versuchen.                                                                                   |
| `429`  | Rate Limit (10 Anfragen pro Sekunde pro Service). Backoff und retry.                                                                                                        |
