# Appointments V1 — Developer Guide

> **Reference:** [Appointments (V1) Endpoint Documentation](https://papidocs.hs1api.com/publicapi/api-appointment-management/appointments-(v1))


## GET — Bulk Appointments

This endpoint retrieves appointments belonging to locations within your organization.

### Recommended Filters

Always narrow your query with at least one filter. The following filter keys are supported:

| Filter | Supported Operators |
|  --- | --- |
| `status` | `==`, `!=` |
| `patient.id` | `==`, `!=` |
| `operatory.id` | `==`, `!=`, `->` |
| `location.id` | `==`, `!=`, `->` |
| `provider.id` | `==`, `!=`, `->` |
| `asap` | `==` |
| `start` | `>`, `<`, `>=`, `<=` |
| `lastModified` | `>`, `<`, `>=`, `<=` |


> See the endpoint reference for the full up-to-date list of supported parameters.


### Important Notes

> **⚠️ Upcoming Breaking Change:** `location.id` (as a list of IDs or `["ALL"]`) will become a **required** filter parameter. Omitting `location.id` currently defaults to all locations.


> **⚠️ Not Recommended:** Do **not** paginate through large data sets using the `page` parameter, even at `pageSize=500`. This approach is slow and unreliable at scale.


> **✅ Recommended:** Use `lastId` pagination instead. See the section below.


## Paginating with `lastId`

The `lastId` parameter is the most efficient way to page through large result sets. It uses the appointment's `id` as a cursor rather than an offset.

### Step 1 — Initial Request

Send your first request without `lastId`:

```
GET /api/v1/appointments?pageSize=500&filter=lastModified>2025-02-16T00:00:00.000Z,location.id->[64000000000004]
```

### Step 2 — Subsequent Requests

Take the `id` of the **last record** in the returned `data` array and pass it as `lastId` in the next request:

```
GET /api/v1/appointments?lastId=8000052360370&pageSize=500&filter=lastModified>2025-02-16T00:00:00.000Z,location.id->[64000000000004]
```

### Step 3 — Repeat

Continue until the response returns an empty `data` array or a no-more-data indicator.

You can combine `lastId` with additional filters to narrow results further — for example, filtering by a specific date range, operatory, location, or provider.

## Streaming API

For real-time changes — such as newly created or updated appointments on a live schedule — use the **Streaming API** instead of polling the Public API on a schedule. The Streaming API pushes changes as they happen and is far more efficient than repeated polling.

## Response Structure

A successful `200` response returns a `BulkAppointmentResponseWrapperV1` object:

| Field | Type | Description |
|  --- | --- | --- |
| `data` | array | Array of `AppointmentV1` objects |
| `warnings` | array | Non-fatal warnings (operation still succeeded) |
| `errors` | array | Field-level or structural errors |
| `meta` | object | Pagination metadata — see `meta.pagination` below |


### `meta.pagination` Object

| Field | Type | Description |
|  --- | --- | --- |
| `lastId` | integer | The `id` of the last record returned. Pass as `lastId` in the next request to continue pagination. |


### `AppointmentV1` Object

Each item in `data` represents a single appointment:

| Field | Type | Access | Description |
|  --- | --- | --- | --- |
| `id` | integer | read-only | Unique appointment identifier |
| `start` | string | read/write | Appointment start time (ISO 8601, e.g. `2024-05-09T12:00:00.000Z`) |
| `end` | string | read-only | Computed end time based on `start` + `duration` |
| `duration` | integer | read/write | Length of the appointment in minutes. Valid range: `5`–`1440` |
| `status` | string | read/write | Appointment status enum. See [status field errors](#status) for valid values |
| `statusId` | number | read-only | Numeric representation of `status` |
| `created` | string | read-only | Timestamp when the appointment was created |
| `lastModified` | string | read-only | Timestamp of the most recent update |
| `bookingType` | string | read/write | Booking type enum: `TREATMENT`, `RECARE`, `NEW_PATIENT`, `EXISTING_PATIENT` |
| `needsFollowUp` | boolean | read/write | Whether the patient requires a follow-up |
| `followedUpOn` | string | read/write | Date follow-up was completed. Requires `needsFollowUp: true` |
| `note` | string | read/write | Internal appointment note |
| `other` | string | read/write | Free-text procedure description. Maximum 90 characters |
| `asap` | boolean | read/write | Whether the appointment is flagged as ASAP |
| `bookedOnline` | boolean | read/write | Whether the appointment was booked online |
| `needsPremedicate` | boolean | read/write | Whether the patient requires premedication |
| `labCaseDentalLab` | object | read/write | Linked model reference to the dental lab. Setting to `null` clears all lab case fields |
| `labCaseStatus` | string | read/write | Lab case status: `OUTSTANDING`, `RECEIVED` |
| `labCaseDueDate` | string | read/write | Lab case due date (must be on or before appointment end time) |
| `labCaseNote` | string | read/write | Note associated with the lab case |
| `provider` | object | read/write | Linked model reference to the primary provider |
| `otherProvider` | object | read/write | Linked model reference to a secondary provider |
| `patient` | object | read/write | Linked model reference to the patient |
| `operatory` | object | read/write | Linked model reference to the operatory |
| `patientProcedures` | array | read/write | Array of linked model references to patient procedures |
| `practiceProcedures` | array | read/write | Array of linked model references to practice procedures |
| `visits` | array | read/write | Array of linked model references to treatment plan visits |
| `timePattern` | object | read-only | Breakdown of provider/assistant/chair time within the appointment |
| `confirmed` | string | read-only | Timestamp when the appointment was confirmed |
| `leftMessage` | string | read-only | Date a message was left for the patient |


## Errors & Warnings Reference

details
summary
strong
HTTP Status Code Errors
— click to expand
These apply to all Appointments V1 endpoints.

#### `400` — Bad Request

The request body or query parameters are invalid or cannot be processed. See the [Field-Level Validation Errors](#field-level-validation-errors) section below for specific causes.

#### `401` — Unauthorized

|  |  |
|  --- | --- |
| **Message** | Authentication is required and has failed or has not been provided. |
| **Why** | No valid auth token was sent, the token is expired, or the token is malformed. |
| **Fix** | Provide a valid Bearer token in the `Authorization` header. |


#### `403` — Forbidden

|  |  |
|  --- | --- |
| **Message** | The request is understood, but it has been refused or access is not allowed. |
| **Why** | The authenticated token does not have the required scope. Read operations require `model:read`; write operations (POST, PUT, DELETE) require `model:write`. |
| **Fix** | Request a token that includes the appropriate scope for the operation you are attempting. |


#### `404` — Not Found

|  |  |
|  --- | --- |
| **Message** | The requested resource is either missing or does not exist. |
| **Why** | No appointment exists for the given `appointmentId` within your organization, or the ID is not a valid integer (e.g. `0` or non-numeric). |
| **Fix** | Confirm the appointment ID is correct and belongs to your organization. |


#### `408` — Request Timeout

|  |  |
|  --- | --- |
| **Message** | The server timed out while processing the request. |
| **Why** | The server took too long to respond, often due to a complex query or high server load. |
| **Fix** | Retry the request. If the issue persists, simplify your query or contact support. |


#### `429` — Too Many Requests

|  |  |
|  --- | --- |
| **Message** | Rate limit exceeded. |
| **Why** | Your client has made too many requests in a short period of time. |
| **Fix** | Implement exponential backoff and retry logic. Reduce request frequency. |


#### `500` — Internal Server Error

|  |  |
|  --- | --- |
| **Message** | An unexpected error occurred. |
| **Why** | An unhandled server-side error occurred. |
| **Fix** | Retry the request. If the issue persists, contact support with the request details and timestamp. |


details
summary
strong
Field-Level Validation Errors
— click to collapse
These errors are returned in the `errors` array of the response body alongside a `400` status code.

### `start`

**Error:** `start` is required and must not be empty.

|  |  |
|  --- | --- |
| **Why** | The `start` field is required on POST. It was omitted or sent as an empty string. |
| **Fix** | Provide a valid ISO 8601 datetime string, e.g. `2025-06-15T09:00:00.000Z`. |


**Error:** `start` has an invalid date format.

|  |  |
|  --- | --- |
| **Why** | The datetime string was provided in an unsupported format. Formats such as `YYYY-MM-DDTHH`, `YYYY-MM-DDTHH:mm`, and `YYYY-MM-DDTHH:mm:ss` are all rejected — they are missing the milliseconds/offset component. |
| **Fix** | Use a full ISO 8601 datetime format: `YYYY-MM-DDTHH:mm:ss.sssZ`. |


**Error:** `start and end should be at the same day`

|  |  |
|  --- | --- |
| **Why** | The computed end time (`start + duration`) crosses midnight into the next calendar day in the location's timezone. |
| **Fix** | Adjust `start` or `duration` so the appointment does not span midnight. |


### `duration`

**Error:** `duration` must be an integer between `5` and `1440`.

|  |  |
|  --- | --- |
| **Why** | The `duration` value (in minutes) is outside the allowed range. Values below `5` or above `1440` (24 hours) are rejected. |
| **Fix** | Provide a `duration` value between `5` and `1440` inclusive. |


### `status`

**Error:** Invalid enum value for `status`.

|  |  |
|  --- | --- |
| **Why** | The value provided for `status` is not recognized. |
| **Fix** | Use one of the following valid values: `UNCONFIRMED`, `CONFIRMED`, `LATE`, `HERE`, `READY`, `CHAIR`, `CHECKOUT`, `COMPLETED`, `WILL_CALL`, `NO_SHOW`, `BROKEN`, `LEFT_MESSAGE`, `UNREACHABLE`, `CANCELLEDBYOFFICE`. |


**Error:** `Appointment cannot be completed due to missing email for Patient`

|  |  |
|  --- | --- |
| **Field** | `status` |
| **Why** | `status` is being set to `COMPLETED`, the location has `workflowComplianceReminderEnabled = true`, and the patient has no `emailAddress` on file. Additionally, either no `COLLECT_EMAIL` AppointmentTask exists for this appointment, or the task has not been skipped with a `skippedReason`. |
| **Fix** | Add an email address to the patient record, **OR** ensure a `COLLECT_EMAIL` AppointmentTask exists with a `skippedReason` set before marking the appointment `COMPLETED`. |


**Error:** `Appointment cannot be completed due to missing phone for Patient`

|  |  |
|  --- | --- |
| **Field** | `status` |
| **Why** | `status` is being set to `COMPLETED`, the location has `workflowComplianceReminderEnabled = true`, and the patient has no phone numbers on file. Additionally, either no `COLLECT_PHONE` AppointmentTask exists for this appointment, or the task has not been skipped. |
| **Fix** | Add a phone number to the patient record, **OR** ensure a `COLLECT_PHONE` AppointmentTask exists with a `skippedReason` set before marking the appointment `COMPLETED`. |


### `other`

**Error:** `other` exceeds maximum length of 90 characters.

|  |  |
|  --- | --- |
| **Why** | The `other` free-text field has a maximum length of 90 characters. |
| **Fix** | Shorten the `other` value to 90 characters or fewer. |


### `needsFollowUp` / `followedUpOn`

**Error:** `needsFollowUp should have value 'true' for setting followedUpOn date`

|  |  |
|  --- | --- |
| **Field** | `needsFollowUp` |
| **Why** | A `followedUpOn` date was provided but `needsFollowUp` is `false` or not set. The `followedUpOn` date is only valid when `needsFollowUp` is `true`. |
| **Fix** | Set `needsFollowUp: true` alongside `followedUpOn`, or omit `followedUpOn` entirely if follow-up is not needed. |


### `labCaseDueDate`

**Error:** `labCaseDueDate cannot be after the end date of the Appointment`

|  |  |
|  --- | --- |
| **Field** | `labCaseDueDate` |
| **Why** | The `labCaseDueDate` you supplied is after the appointment's computed `end` datetime. Lab case work must be completed before the appointment ends. |
| **Fix** | Set `labCaseDueDate` to a date on or before the appointment's end time. |


### `practiceProcedures` / `patientProcedures` / `visits` / `other`

**Error:** `Either other, practiceProcedures, patientProcedures, or visits must be provided`

|  |  |
|  --- | --- |
| **Why** | An appointment must have at least one of: `other`, `practiceProcedures`, `patientProcedures`, or `visits`. All four are absent or empty. |
| **Fix** | Include at least one of `other`, `practiceProcedures`, `patientProcedures`, or `visits` in your request. |


### `patientProcedures`

**Error:** `PatientProcedures with ids [X, Y] are completed and cannot be assigned to the Appointment`

|  |  |
|  --- | --- |
| **Field** | `patientProcedures` |
| **Why** | One or more of the provided `patientProcedure` IDs already have a `COMPLETED` status. Completed procedures cannot be re-scheduled. |
| **Fix** | Only link patient procedures that are not yet completed. |


**Error:** `PatientProcedures with ids [X, Y] are created for another Patient and cannot be assigned to the Appointment with different Patient`

|  |  |
|  --- | --- |
| **Field** | `patientProcedures` |
| **Why** | The patient procedures you provided belong to a different patient than the one linked to this appointment. |
| **Fix** | Ensure all `patientProcedures` belong to the same patient specified in the appointment's `patient` field. |


### `visits`

**Error:** `Visits with ids [X, Y] are completed and cannot be assigned to the Appointment`

|  |  |
|  --- | --- |
| **Field** | `visits` |
| **Why** | One or more of the visits you are linking have all of their procedures in `COMPLETED` status, making the visit effectively complete. Completed visits cannot be re-scheduled. |
| **Fix** | Only link visits that still have at least one non-completed procedure. |


**Error:** `Visits with ids [X, Y] are created for another Patient and cannot be assigned to the Appointment with different Patient`

|  |  |
|  --- | --- |
| **Field** | `visits` |
| **Why** | The tx plan visits you provided belong to a treatment case for a different patient than the one on this appointment. |
| **Fix** | Ensure all `visits` belong to the same patient as the appointment. |


**Error:** `Visit with id 'X' already contains PatientProcedure with id 'Y' so they cannot be assigned separately to the same Appointment`

|  |  |
|  --- | --- |
| **Why** | You included both a `visit` and one of that visit's own `patientProcedures` as separate line items on the same appointment. This creates a duplicate association. |
| **Fix** | Include either the visit **or** its individual procedures — not both on the same appointment. |


### `operatory`

**Error:** `OperatoryV1 not found`

|  |  |
|  --- | --- |
| **Why** | No `operatory` linked model was provided, or the operatory ID does not exist within your organization. |
| **Fix** | Provide a valid `operatory` linked model with an ID that exists in your organization. |


**Error:** `The specified operatory is inactive. Appointment cannot be scheduled.`

|  |  |
|  --- | --- |
| **Field** | `operatory.active` |
| **Why** | The operatory you are assigning has `active: false`. |
| **Fix** | Use an active operatory, or reactivate the operatory before scheduling. |


**Error:** `location of Appointment and location of Operatory should be the same`

|  |  |
|  --- | --- |
| **Field** | `operatory` |
| **Why** | On PUT, the new operatory belongs to a different location than the appointment's existing location. An appointment's location is derived from its operatory and cannot be changed via an operatory swap. |
| **Fix** | To move an appointment to a different location, delete and recreate it at the correct location. To change operatories, use one from the same location. |


### `provider` / `otherProvider`

**Error:** `Provider should have 'isNonPersonEntity: false' to be assigned to the appointment`

|  |  |
|  --- | --- |
| **Field** | `provider.isNonPersonEntity` or `otherProvider.isNonPersonEntity` |
| **Why** | The provider is flagged as a non-person entity (e.g. a room or equipment resource). Only person-type providers can be assigned to appointments. |
| **Fix** | Use a provider with `isNonPersonEntity: false`. |


**Error:** `Provider should have 'active: true' to be assigned to the appointment`

|  |  |
|  --- | --- |
| **Field** | `provider.active` or `otherProvider.active` |
| **Why** | The provider linked to `provider` or `otherProvider` is currently inactive. |
| **Fix** | Use an active provider, or reactivate the provider before assigning them. |


### Linked Model "Not Found" Errors

These fire when a referenced linked model ID cannot be found within your organization.

**Error:** `Linked model with id 'X' of type '<DomainType>' not found`

|  |  |
|  --- | --- |
| **Why** | A single linked model reference (e.g. `patient`, `provider`, `operatory`, `labCaseDentalLab`) was provided with an ID that does not exist in your organization. |
| **Fix** | Verify the ID is correct and belongs to your organization. |


**Error:** `Linked models with ids [X, Y] of type '<DomainType>' not found`

|  |  |
|  --- | --- |
| **Why** | One or more IDs in a linked model array (e.g. `patientProcedures`, `practiceProcedures`, `visits`) do not exist in your organization. |
| **Fix** | Verify all IDs in the array exist and belong to your organization. |


> **Common `<DomainType>` values:** `LocationV1`, `PatientV1`, `ProviderV1`, `OperatoryV1`, `DentalLabV1`, `PracticeProcedureV1`, `PatientProcedureV1`, `VisitV1`


### Internal / Related Data Errors

**Error:** `Related data is absent for the appointment`

|  |  |
|  --- | --- |
| **HTTP Status** | `500` |
| **Why** | During audit record creation, an expected relationship (e.g. `patient` or `operatory`) was unexpectedly `null` on the appointment record. This is an internal consistency error and should not occur under normal usage. |
| **Fix** | This typically indicates a data integrity issue. Contact support. |


details
summary
strong
Warnings
— click to collapse
Warnings are returned in the `warnings` array of the response body when an operation **succeeds** but there are recommended follow-up actions. They do **not** cause the request to fail.

### Warning — Missing `COLLECT_EMAIL` AppointmentTask

|  |  |
|  --- | --- |
| **Title** | `Required operation` |
| **Description** | `AppointmentTask with type 'COLLECT_EMAIL' is missing for this appointment and should be created` |
| **Why** | The appointment was created or updated with `status = COMPLETED`, the location has `workflowComplianceReminderEnabled = true`, but no `COLLECT_EMAIL` task has been created for this appointment. |
| **Fix** | Create an AppointmentTask of type `COLLECT_EMAIL` for this appointment. |


### Warning — Missing `COLLECT_PHONE` AppointmentTask

|  |  |
|  --- | --- |
| **Title** | `Required operation` |
| **Description** | `AppointmentTask with type 'COLLECT_PHONE' is missing for this appointment and should be created` |
| **Why** | The appointment was created or updated with `status = COMPLETED`, the location has `workflowComplianceReminderEnabled = true`, but no `COLLECT_PHONE` task has been created for this appointment. |
| **Fix** | Create an AppointmentTask of type `COLLECT_PHONE` for this appointment. |


> **⚠️ Important:** These warnings are informational — the operation still succeeds. However, if the appointment is subsequently moved to `COMPLETED` status without the tasks being resolved (completed or skipped with a `skippedReason`), the update will be **blocked with a `400` error**. See the [`status` field errors](#status) above.