API reference / Appointments
Bookings, conflicts, and availability
The appointments resource covers bookings, chair-blocking entries, and a derived availability endpoint that computes free slots for a chair on a given day. This page lists the object shape, the conflict-detection rule, and every endpoint's fields and errors.
Who can do what
Every endpoint requires auth and is scoped to the caller's practice. Unlike chairs and practice settings, appointments are not owner-only — any practice member, owner or assistant, can create, update, and delete them. appointmentId path segments must be canonical UUIDs; a malformed one returns not_found (404) without reaching the service layer.
The appointment object and its type
An appointment carries chairId, patientId, type, title, startAt, endAt, color, serviceRef, notes, createdBy, and metadata, plus id, practiceId, createdAt, updatedAt. type is one of appointment, reminder, or unavailable. An unavailable entry blocks a chair the same way a real appointment does for conflict purposes; a reminder entry never occupies a chair and is exempt from both the overlap check and the availability calculation, even if it has a chairId set.
Overlap (double-booking) detection
When chairId is set and type !== "reminder", creating or updating an appointment is rejected with conflict (409) if it overlaps an existing appointment or unavailable block on the same chair. Overlap uses strict inequality (existing.startAt < newEnd AND existing.endAt > newStart), so two entries that touch exactly at a boundary are not considered overlapping. Different chairs never conflict with each other, and on PATCH the appointment being updated excludes itself from its own conflict check.
Reminders as a side effect
Creating, updating, or deleting an appointment-type entry that has a patientId automatically schedules, reschedules, or cancels a background appointment-reminder task, subject to the practice's configured lead time — there is no separate API call to manage it. The delivery mechanics (channels, degraded-mode behavior) live in the notifications documentation rather than here.
See the error reference for what every code and status means, and the API reference hub for the other resources.
Endpoints
GET/api/v1/appointments
Lists appointments starting inside a time window, ordered by start time.
The window is half-open: an appointment is included when its startAt falls in [from, to). An appointment that started before from and is still running is not returned.
Query parameters
| Field | Type | Default | Description |
|---|---|---|---|
| fromRequired | ISO datetime | — | Start of the window, inclusive. |
| toRequired | ISO datetime | — | End of the window, exclusive. |
| chairId | uuid | — | Restricts the result to a single chair. |
Response · 200
{
"data": {
"appointments": [
{
"id": "a7c30e51-....",
"type": "appointment",
"startAt": "2026-07-08T10:00:00.000Z",
"endAt": "2026-07-08T11:00:00.000Z"
}
]
},
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| validation_failed | 400 | from or to is missing or not a valid datetime, or chairId is malformed. |
| missing_authorization | 401 | No Authorization header was sent. |
POST/api/v1/appointments
Creates an appointment, a reminder, or a chair-blocking unavailable entry.
Any practice member can do this — there is no owner-only gate on appointments, unlike chairs and practice settings.
A chairId or patientId belonging to another practice is rejected as validation_failed, not surfaced as a database error.
Body
| Field | Type | Description |
|---|---|---|
| typeRequired | string | One of appointment, reminder, unavailable. |
| startAtRequired | ISO datetime | When it starts. |
| endAtRequired | ISO datetime | Must be strictly after startAt. |
| chairId | uuid | Must belong to your practice. Required for the entry to occupy a chair. |
| patientId | uuid | Must belong to your practice. Setting it on an appointment schedules the reminder. |
| title | string | Trimmed; must not be empty if present. |
| color | string | Display colour in the calendar. |
| serviceRef | string | Reference to the service being performed. |
| notes | string | Free text. |
Example request
{
"type": "appointment",
"startAt": "2026-07-08T10:00:00.000Z",
"endAt": "2026-07-08T11:00:00.000Z",
"chairId": "5c2f9b74-....",
"title": "Checkup"
}Response · 201
{
"data": {
"id": "a7c30e51-....",
"type": "appointment",
"startAt": "2026-07-08T10:00:00.000Z",
"endAt": "2026-07-08T11:00:00.000Z"
},
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| validation_failed | 400 | endAt is not after startAt, a chairId or patientId is unknown, or an unrecognized field was sent. |
| conflict | 409 | The entry overlaps an existing appointment or unavailable block on the same chair. |
GET/api/v1/appointments/:appointmentId
Fetches one appointment.
Path parameters
| Field | Type | Description |
|---|---|---|
| appointmentIdRequired | uuid | Canonical UUID. |
Response · 200
{
"data": { "id": "a7c30e51-....", "type": "appointment" },
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| not_found | 404 | No such appointment in this practice. |
PATCH/api/v1/appointments/:appointmentId
Updates an appointment. Every field from the create body, all optional.
endAt > startAt is re-checked whenever either changes, against the existing value of the other if only one is sent. The conflict check re-runs against the post-update chair and time range, excluding this appointment from its own check.
Path parameters
| Field | Type | Description |
|---|---|---|
| appointmentIdRequired | uuid | Canonical UUID. |
Example request
{
"startAt": "2026-07-08T10:30:00.000Z",
"endAt": "2026-07-08T11:30:00.000Z"
}Response · 200
{
"data": {
"id": "a7c30e51-....",
"startAt": "2026-07-08T10:30:00.000Z",
"endAt": "2026-07-08T11:30:00.000Z"
},
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| validation_failed | 400 | An invalid field, an unknown chairId or patientId, or an unrecognized field. |
| conflict | 409 | The new time overlaps a different entry on the same chair. |
| not_found | 404 | No such appointment in this practice. |
DELETE/api/v1/appointments/:appointmentId
Deletes an appointment, cancelling any reminder scheduled for it.
Path parameters
| Field | Type | Description |
|---|---|---|
| appointmentIdRequired | uuid | Canonical UUID. |
Response · 200
{
"data": { "deleted": true },
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| not_found | 404 | No such appointment in this practice. |
GET/api/v1/appointments/availability
Computes the free slots on one chair for one calendar day.
Slots are bounded by the practice's working hours and slot length, falling back to defaults for any unset setting. Busy blocks are the appointment and unavailable entries overlapping that local day; reminder entries are excluded, because they do not occupy a chair.
Query parameters
| Field | Type | Default | Description |
|---|---|---|---|
| dateRequired | string | — | YYYY-MM-DD, interpreted in the practice's timezone. |
| chairIdRequired | uuid | — | Must belong to your practice. |
| durationMinutesRequired | integer | — | Length of slot to look for, 1–1440. |
Response · 200
{
"data": {
"slots": [
{ "startAt": "2026-07-08T06:00:00.000Z", "endAt": "2026-07-08T07:00:00.000Z" }
]
},
"requestId": "req_5f2c1e40-...."
}Errors
| Code | Status | When |
|---|---|---|
| validation_failed | 400 | date is missing or malformed, chairId is not a UUID or belongs to another practice, or durationMinutes is out of range. |
| not_found | 404 | The practice could not be resolved. |
Common questions
Patient records are encrypted in transit and at rest, files are stored privately and served through short-lived signed links, and every change is written to an audit log. Staff accounts sign in with passkeys rather than shared passwords.
Any authenticated practice member can create, update, and delete appointments — owner and assistant roles both have full access, with no role gate on those actions. This is different from chairs and practice settings, which are owner-only, so it's worth checking per resource rather than assuming one rule applies everywhere.
Two entries on the same chair whose time ranges overlap under a strict-inequality rule, where touching exactly at a boundary does not count as overlapping. Reminder-type entries are exempt entirely, even with a chairId set, since they don't occupy a chair. A conflicting create or update is rejected with a 409 conflict response.
No. Creating, updating, or deleting a patient-linked appointment automatically schedules, reschedules, or cancels its reminder in the background, based on the practice's configured lead time. There is no separate endpoint to call for this — it happens as a side effect of the normal appointment write endpoints.