> ## 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

> Let patients discover appointment types, book slots, and join video appointments

# Scheduling

The Scheduling API lets telemedicine partners offer patient self-booking for video appointments. Public discovery endpoints can be called with a `shop_uid`. Booking and rebooking operations use either an organisation API key or a short-lived patient booking token.

<Warning>
  Never put signed patient booking tokens in URLs. Exchange the token for a `booking_launch_code` and use that code for hosted booking links or embedded widgets.
</Warning>

## Authentication

The Scheduling API accepts two credentials. Most endpoints accept either; some require a specific one.

### Patient booking tokens (recommended for patient flows)

A short-lived JWT minted by the partner backend for a specific patient. Sent in the `X-RxScale-Booking-Token` header. The token is verified against an organisation booking-token secret provisioned by RxScale.

**Algorithm:** `HS256`

**JOSE header:** must include `kid` — the `key_id` returned when you provisioned the booking-token secret.

**Required claims:**

| Claim                    | Type   | Description                                                                                                     |
| ------------------------ | ------ | --------------------------------------------------------------------------------------------------------------- |
| `aud`                    | string | Must be `rxscale-scheduling`.                                                                                   |
| `exp`                    | int    | Expiration (Unix seconds). Keep this short — under 5 minutes is typical.                                        |
| `nbf`                    | int    | Not-before (Unix seconds).                                                                                      |
| `iat`                    | int    | Issued-at (Unix seconds).                                                                                       |
| `shop_identifier`        | string | The shop identifier you configured in RxScale.                                                                  |
| `shop_customer_id`       | string | Your stable customer identifier. RxScale uses it to deduplicate patients across booking sessions.               |
| `patient_profile_fields` | array  | Patient profile fields to upsert. Each entry: `{ "field": "<field_key>", "value": <value>, "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 are stored **encrypted at rest** in the RxScale database. Only your `key_id` is stored in plaintext for routing; the secret value is decrypted in-memory both at verification time and whenever an admin reads the value back through the admin portal or the secrets API. Rotate by provisioning a new secret and revoking the old one once your minter has switched over.
</Note>

### Organisation API key

Send `X-API-Key: <key>` instead of the booking token for server-to-server flows. The key needs the `scheduling:write` permission. Use this for back-office tooling, scripted rebooking, or operational scripts.

## List Appointment Types

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

<ParamField query="shop_uid" type="string" required>
  Shop UID used to scope appointment types to the correct organisation.
</ParamField>

No API key is required.

```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": "Video consultation",
      "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"
    }
  ]
}
```

## Search Slots

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

No API key is required.

Slot search only returns slots that satisfy the appointment type's effective
booking notice. If a doctor has a doctor-specific override for that appointment
type, the doctor's value takes precedence, including `0` minutes. Appointment
types configured as `selected_doctors_only` only return doctors with an active
doctor-specific setting.

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

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

<ParamField body="from" type="int" required>Window start (Unix seconds).</ParamField>
<ParamField body="to" type="int" required>Window end (Unix seconds). Max 31 days from `from`.</ParamField>
<ParamField body="doctor_uid" type="string">Restrict to a single doctor.</ParamField>
<ParamField body="timezone" type="string">IANA timezone (e.g. `Europe/Berlin`) used to localize the output. Defaults to `Europe/Berlin`. Each slot includes `start_local`/`end_local` rendered in this zone; `start_date`/`end_date` remain Unix seconds.</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"
    }
  ]
}
```

## Create Booking Session

Use this endpoint when launching the hosted UI or script widget. The request can include the signed patient booking token either in `X-RxScale-Booking-Token` or in the JSON body as `booking_token`.

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

<ParamField body="mode" type="string" required>`booking` for a new appointment, `rebooking` for an existing one.</ParamField>
<ParamField body="appointment_type_uid" type="string">Required for `booking` mode.</ParamField>
<ParamField body="from" type="int">Window start (Unix seconds). Required for `booking` mode.</ParamField>
<ParamField body="to" type="int">Window end (Unix seconds). Required for `booking` mode.</ParamField>
<ParamField body="return_url" type="string">URL to redirect the patient to after a successful booking.</ParamField>
<ParamField body="booking_token" type="string">Patient booking JWT, if not sent via header.</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..."
}
```

## Get Booking Session

The hosted UI calls this with just the launch code to load the booking context. No authentication header is needed — the launch code itself is the 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": "Video consultation",
    "duration_minutes": 15
  },
  "start_from": 1778457600,
  "start_to": 1778544000,
  "mode": "booking",
  "return_url": "https://partner.example/booking-complete",
  "expires_at": 1778458500
}
```

The response intentionally omits `patient_profile_uid` and other patient identifiers, so the launch code can be safely passed through URLs in the patient's browser.

## Create and Confirm Holds

Authenticated integrations can create holds with an API key that has `scheduling:write`. Hosted UI integrations use the `booking_launch_code` routes.

```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 seconds.</ParamField>
<ParamField body="visit_reason" type="string">Optional reason for the appointment, up to 2000 characters. Blank values are stored as `null`.</ParamField>
<ParamField header="Idempotency-Key" type="string">Optional. Repeating the same key with the same body returns the existing hold instead of creating a duplicate.</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": "Medication review before changing dosage"
  }'
```

```json theme={null}
{
  "hold_uid": "pdm_123",
  "expires_at": 1778490900,
  "ttl_seconds": 900,
  "status": "held",
  "visit_reason": "Medication review before changing dosage"
}
```

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

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

```json theme={null}
{
  "uid": "pdm_123",
  "status": "confirmed",
  "start_date": 1778490000,
  "end_date": 1778490900,
  "visit_reason": "Medication review before changing dosage"
}
```

<Note>
  If a hold has expired but no other appointment has taken the slot, confirmation is still allowed and the appointment moves to `confirmed`.
</Note>

<Note>
  When present, `visit_reason` is visible to doctors/admins and can appear in appointment reminder email/SMS content and synced calendar invite descriptions.
</Note>

## Cancel Appointment

Cancel a confirmed or held appointment. Accepts either a booking token (patient-driven cancel) or an organisation API key (operations).

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

<ParamField body="reason" type="string">Optional free-text reason. Stored on the appointment for audit.</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"
}
```

Patient cancellations honour the appointment type's `cancellation_min_notice_minutes`. If you cancel inside the notice window the API returns a `400` with the offending field; rebook the patient or call with an API key for an operational override.

## Rebook Appointment

Move an existing appointment to a new slot. The rebook flow allocates a new appointment row (with `previous_meeting_uid` set to the original) and cancels the old one atomically.

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

<ParamField body="new_start_date" type="int" required>Unix seconds for the new slot.</ParamField>
<ParamField body="new_doctor_uid" type="string">Required when the appointment type's `rebooking_mode` is `any_doctor`. For `same_doctor_only`, the original doctor is reused.</ParamField>
<ParamField body="appointment_type_uid" type="string">Optional. Defaults to the original appointment type.</ParamField>
<ParamField header="Idempotency-Key" type="string">Strongly recommended — protects against double-rebooks during 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"
}
```

Rebooks are blocked if the appointment type sets `allow_patient_rebooking: false`, if `rebooking_min_notice_minutes` has not been met, or if the new slot is taken.

## Get Join Token

Returns a short-lived Jitsi waiting-room JWT and the room name. Used by the patient hosted UI to launch the video call.

```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
}
```

### Join window

The endpoint accepts join requests from **10 minutes before** the appointment `start_date` until **60 minutes after** the appointment `end_date`. Requests outside that window return:

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

Surface this clearly in your hosted UI — the patient should see a countdown or "join opens in N minutes" hint rather than a blocked button.

## Hosted UI

Redirect patients to the `launch_url` returned by the booking session endpoint, or embed the widget script:

```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>
```

The widget renders the hosted booking page in an iframe inside a Shadow DOM wrapper so styles do not leak into the host page.

## Doctor Portal Endpoints

These endpoints are scoped to the authenticated doctor (Auth0 token), used by the RxScale Doctor
Portal. Partners typically don't need to call them directly.

### List Scheduled Appointments

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

Query parameters mirror the admin list endpoint: `status`, `from`, `to`, `patient_uid`,
`page`, `limit`. The response is the doctor's own appointments only.

### Availability Rules 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}
```

Each rule has `weekday` (0 = Monday … 6 = Sunday), `start_time` and `end_time` in minutes since
midnight, an optional `buffer_minutes` between consecutive slots, and optional `valid_from` /
`valid_until` Unix-second bounds. PATCH bodies are partial; pass `clear_valid_from: true` or
`clear_valid_until: true` to drop a previously-set bound. DELETE is a soft delete.

## Admin Endpoints

These endpoints are scoped to the authenticated organisation admin (Auth0 token).

### List Appointments

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

Query parameters: `doctor_uid`, `patient_uid`, `status`, `from`, `to`, `page`, `limit`. By default
returns active (held + confirmed) appointments scheduled from now onwards; pass `status=all` and
`from=0` to see history.

### Cancel Appointment

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

Requires `{"reason": "..."}` in the body.

### Appointment Type Reminders

```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}
```

Each reminder has `recipient_role` (`patient` / `doctor` / `admin`), `minutes_before` (positive
integer up to 60 days), `send_email`, `send_sms`, and `active`. Multiple reminders per
`(appointment_type, recipient_role)` are allowed as long as the `minutes_before` offset differs.

A maintenance job runs every minute, finds confirmed appointments whose firing window includes
"now", and publishes one `scheduling.appointment_reminder_due` event per resolved recipient.
The published event always fires (partner webhook subscribers receive it); RxScale's own
notification handler only dispatches email/SMS when the corresponding `send_email` / `send_sms`
flag is set on the reminder row.

If the appointment has a `visit_reason`, the reminder event and RxScale email/SMS content include it.

Partners subscribe to the event via the existing organisation notification subscription endpoint
with `notification_type=APPOINTMENT_REMINDER_DUE`.

### View Doctor Availability

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

Read-only listing of a doctor's weekly bookable windows. Returns `404` if the doctor is not in
the admin's organisation.

### 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` provisions a new secret and returns `{key_id, secret}`. `GET` returns the secret value
decrypted — RxScale stores secrets encrypted at rest, but admins can re-read them any time from
this endpoint (and from the admin portal Settings → Booking secrets page) so a new minter can be
configured without re-provisioning. `DELETE` revokes the secret.

The legacy organisation-scoped paths
(`/v1/admin/scheduling/organisations/{organisation_uid}/booking-token-secrets[/<key_id>]`) are
still accepted and validate against the authenticated organisation.

## Errors

The Scheduling API uses standard HTTP status codes:

| Status | Meaning                                                                                                                                           |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Schema validation failed, or a business rule blocked the action (see the `error` body for the field/message).                                     |
| `401`  | Booking token or API key missing or invalid.                                                                                                      |
| `404`  | Resource not found, **or** the caller is not authorised to see it. The API returns `404` for unauthorised access to prevent resource enumeration. |
| `409`  | Slot taken by another patient between hold and confirm. Re-search and try again.                                                                  |
| `429`  | Rate limit (10 requests per second per service). Back off and retry.                                                                              |
