Skip to main content
Goal: a “Book now” widget on the practice’s website. The visitor picks a location and appointment type, sees free times, enters their details and books. The practice sees the booking in its calendar exactly as if the front desk had made it, and the patient gets the normal confirmation messages. Key: read & write. Resources used: locations, appointment_types, practitioners, availability, patients, appointments.

The flow

Never call the API from the browser. The key is a server secret; the widget talks to your server, which talks to PracticeHub. Everything below runs server-side (Node 18+, no dependencies beyond fetch).

A tiny client

Every example on this site uses the same shape: one function that adds the base URL and bearer token, parses the { message, code, errors } error body, and retries 429/5xx with backoff.

1. Reference data (cache it)

Locations, appointment types and practitioners change rarely. Load them at startup and refresh every few minutes (or on the locations.updated / appointment_types.updated webhooks).
Only offer locations and types with online_booking: true — that is the practice’s own “bookable online” switch, and /availability in the default visibility=online view refuses anything else with 422 not_online_bookable.

2. Free slots for a week

Render slots grouped by day; grey out meta.unavailable_dates. Slots are already restricted to what the practice allows online, at most 50 per day, in meta.timezone. Add practitioner_id=109 to the query if the visitor chose a practitioner. Windows are capped at 31 days — page week by week.

3. Find or create the patient

Booking needs a patient_id. Match on email first (the practice’s records may already have this person), and tag anything you create so it is traceable:
Phone numbers are normalised on the way in (intl_number, country code) exactly as when staff type them. A deleted patient cannot be booked (422 patient_not_found) — show a “please call the practice” message rather than retrying. patients.blocked is the practice’s online booking block: the API does not enforce it (staff can still book), so a public widget should check it on the found patient and route blocked patients to the front desk.

4. Book — safely

Three things make this safe:
  • Copy the slot verbatim. start, end, practitioner_id, location_id, appointment_type_id, resource_id come straight from /availability; don’t recompute end from a duration you think you know.
  • Idempotency-Key = your own booking reference. If the visitor’s connection drops after PracticeHub booked but before your server heard, a retry with the same key returns the original 201 (with Idempotent-Replayed: true) instead of a double booking. Retrying a different slot is a new key.
  • A clash is normal. Two visitors can pick the same slot; the second gets 409 slot_unavailable. Re-query and offer the next one — don’t send allow_clash: true from a public widget.
The booking is validated as in the calendar (active patient/location/practitioner, resource at that location, group capacity), lands as status: "pending", appears in the practice’s calendar and board immediately, triggers confirmation/reminder messaging, and the appointment log reads “Created via API: Booking widget”.

5. Manage the booking

Give the visitor a “manage my booking” link that maps to your bookingRefappointment.id.
A PATCH on a cancelled, missed or processed (checked-out) appointment is refused with 409 appointment_rule and a message saying why — show it; the practice’s own rules apply to you as they do to staff.

Gotchas

Going further

  • Subscribe to appointments.cancelled / appointments.rescheduled webhooks to keep your “manage my booking” page in step when staff change things.
  • Store your reference in metadata.widget_ref (done above) so support can find a booking from either side: GET /appointments?metadata[widget_ref]=eq:….
  • Show online_fee from the slot if the practice charges for online bookings; taking payment is a separate flow (payments + payment_allocations) — see the accounting example for the shapes.