# Visits V1 — Developer Guide

> **Reference:** [Visits (V1) Endpoint Documentation](https://papidocs.hs1api.com/publicapi/api-miscellaneous/visits-(v1))


Treatment-plan **visits** group patient procedures within a treatment case (`TxCaseV1`). They are distinct from ledger or payment “visit” groupings. This guide covers the published **Visits V1** operations: bulk list, get by id, create, update (including delete via empty `procedures`), and total count.

All Visits V1 endpoints require an `Organization-ID` header.

Visits do not expose a dedicated `DELETE` method or an async bulk endpoint. To remove a visit, use **PUT** with `procedures: []` (see [Delete via empty procedures](#delete-via-empty-procedures)).

Visits can be linked onto appointments via the appointments API — see **[Appointments V1 — Developer Guide](/publicapi/endpoints/appointments-v1)**.

> **⚠️ Rate Limited:** Visits endpoints are rate limited. See the [rate limiting guide](https://papidocs.hs1api.com/publicapi/api-consumer-guide#rate-limiting) for details.


GET — Bulk List Visits
— click to expand
Returns a paginated list of visits matching the filter criteria.

### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `Organization-ID` | header | Yes | string | The organization ID |
| `filter` | query | No | string | Filter criteria (see filter keys below) |
| `responseFields` | query | No | string | URL-encoded comma-delimited list of fields to include, or `ALL` |
| `page` | query | No | string | Page number for pagination (**deprecated**) |
| `pageSize` | query | No | string | Number of records per page (max `500`) |
| `lastId` | query | No | string | Largest `visitId` from the previous page — used for cursor-based pagination |


### Filter Keys

| Filter Key | Operators | Example |
|  --- | --- | --- |
| `txCase.id` | `==`, `->` (single id) | `?filter=txCase.id==12` or `?filter=txCase.id->[12]` |
| `lastModified` | `>`, `>=`, `<`, `<=` | `?filter=lastModified>=2025-01-01T00:00:00Z` |


`->` is accepted and is equivalent to `==` when the array has **one** id. Do **not** pass multiple ids such as `txCase.id->[12,13]`: the server loads a single case and applies that case’s latest option only.

> **⚠️ Latest option only — easy to misread:** `txCase.id` does **not** mean “all visits for case X.” The server replaces the case id with the **last** `txOption` on that case. Visits that still belong to earlier options on the same case **will not match**. If the case does not exist in the organization, the query returns no matches.


### Important Notes

> **⚠️ Date-limiting filters:** If you do not supply a date-limiting filter, one may be applied automatically using `lastModified`. Supply your own date bound for predictable results. The usable date-limiting filter key for this endpoint is **`lastModified`**.


> **⚠️ Narrow your query:** A very wide `lastModified` range with no other scoping (for example without `txCase.id`) can return **408** (timeout) on large tenants. Combine `txCase.id` with a date bound, or use `lastId` pagination.


> **⚠️ Not Recommended:** Do **not** paginate large data sets using the `page` parameter. Use **`lastId`** cursor pagination instead.


> **Ordering:** Bulk GET does **not** apply a `lastModified` sort (Visit is not in the domain-field lastModified DESC list). `lastId` is an ID-based cursor: `id > previousId` (ascending). Pass the **largest** `id` from the previous page. Do not assume appointments-style “newest first + lastId.”


> **Empty results:** **200** with `data: []` means no visits matched; it does not indicate invalid filter values.


### Example Requests

Fetch visits for a treatment case modified after a date:

```
GET /api/v1/visits?pageSize=100&filter=txCase.id==12,lastModified>=2025-01-01T00:00:00Z
```

### Paginating with `lastId`

`lastId` uses the visit's `id` as a cursor: the next page is `id > lastId`. Pass the **largest** `id` from the previous page.

#### Step 1 — Initial request

```
GET /api/v1/visits?pageSize=100&filter=lastModified>=2025-01-01T00:00:00Z,txCase.id==12
```

#### Step 2 — Subsequent requests

Pass the **largest** `id` in `data` as `lastId`:

```
GET /api/v1/visits?lastId=9000000038237&pageSize=100&filter=lastModified>=2025-01-01T00:00:00Z,txCase.id==12
```

#### Step 3 — Repeat

Continue until `data` is empty.

For how `meta.pagination.total` behaves (including when it is `0` or omitted), see **[Bulk GET — Total Count Behavior](/publicapi/bulk-get-total-count)**.

GET — Visit by ID
— click to expand
Returns a single visit by `visitId`.

### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `visitId` | path | Yes | integer | The visit ID (numeric, minimum `1`) |
| `Organization-ID` | header | Yes | string | The organization ID |
| `responseFields` | query | No | string | Comma-delimited list of fields to include, or `ALL` |


### Example Request

```
GET /api/v1/visits/9000000038237?responseFields=duration,txCase,procedures
```

POST — Create Visit
— click to expand
Creates a new visit on a treatment case. Returns **201** with the created resource in `data`.

The visit is attached to the **latest** treatment option on the given `txCase`. Procedures are **moved** onto this visit (they may be removed from other visits). Moving procedures can empty and delete other visits, and may delete orphaned treatment cases.

### Required Fields

| Field | Type | Description |
|  --- | --- | --- |
| `txCase` | object | Linked model `{ "id": <txCaseId> }` |
| `procedures` | array | Non-empty array of linked patient procedures `{ "id": <procedureId> }` |
| `descProceduresOrder` | boolean | `true` or `false`. Omitting it fails class-validator (`@IsNotEmpty` / `@IsBoolean`) and returns **400**; send it explicitly even when `false`. |


### Optional Fields

| Field | Type | Description |
|  --- | --- | --- |
| `duration` | integer | Length in minutes. Valid range: **`10`–`240`**. Defaults to `60` when omitted. |
| `sequence` | integer | Position among visits on the tx option (`≥ 0`). If omitted or greater than the current count, the visit is appended. |
| `visitProceduresOrderType` | string | Procedure sort order enum (see values below). Optional; database default is `TEETH` when omitted. |


### `visitProceduresOrderType` Values

`TEETH`, `CREATED`, `CODE`, `SURFACES`, `DESCRIPTION`, `PROVIDER`, `AMOUNT`, `CUSTOM`

### Procedure Rules on Create

- Procedures must belong to the **same patient** as the treatment case’s plan.
- Completed, deleted, or voided procedures cannot be moved onto a visit.
- `procedures: []` → **400** (`procedures should not be empty`).
- Omitting `procedures` → **400** (`procedures should be an array and not contain null values`).
- Linked models in the request body only need `{ "id": … }`. Response linked models may also include `type` and `url`.


### Example Request Body

```
POST /api/v1/visits
Content-Type: application/json
```

```json
{
  "txCase": { "id": 12 },
  "procedures": [{ "id": 100 }],
  "duration": 60,
  "visitProceduresOrderType": "TEETH",
  "descProceduresOrder": false
}
```

PUT — Update Visit
— click to expand
Updates an existing visit. Only fields included in the request body are changed; omitted fields are unchanged.

Visits do not expose a dedicated `DELETE` method. To delete a visit, send **PUT** with `procedures: []` — see [Delete via empty procedures](#delete-via-empty-procedures).

### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `visitId` | path | Yes | integer | The visit ID |
| `Organization-ID` | header | Yes | string | The organization ID |
| `responseFields` | query | No | string | Comma-delimited list of fields to include in the response, or `ALL` |


### Writable Body Properties

| Property | Notes |
|  --- | --- |
| `duration` | integer, **10–240** minutes; omitted is unchanged (not reset to 60). Changing duration can update linked appointment duration |
| `sequence` | integer `≥ 0`; reorders sibling visits on the same tx option |
| `visitProceduresOrderType` | enum — omitted is unchanged (not reset to `TEETH`) |
| `descProceduresOrder` | boolean — omitted is unchanged |
| `procedures` | array of `{ id }`; replaces the visit’s procedure set. Empty array deletes the visit |


### Not Writable on PUT

| Property | Behavior |
|  --- | --- |
| `txCase` | Not updatable after create. Sending the same `txCase.id` is a no-op **200**; an invalid `txCase` is **400**. |
| `id`, `lastModified`, `type`, `appointment` | Read-only response fields |


### Scenario-Based Requirements

Visits have **no** value-based conditional required fields (unlike Patients’ `discountType` rules). Required and allowed body keys differ by **operation and tx-case state**:

| Scenario | Required / allowed body |
|  --- | --- |
| **POST create** | Required: `txCase`, `descProceduresOrder`, and a non-empty `procedures` array. Optional: `duration`, `sequence`, `visitProceduresOrderType` (database default `TEETH`). `procedures: []` → **400**. Omitting `descProceduresOrder` → **400**. |
| **PUT update (open case)** | Partial update — send **at least one** mutable field. Empty `{}` → **400** `"No data for updating is provided"`. Omitted fields (including `duration`, `visitProceduresOrderType`, `descProceduresOrder`) are unchanged — create-time defaults are not applied. `txCase` is not updatable. |
| **PUT delete** | `procedures: []` (empty array is the delete signal). Response `data` is `{ id }`. |
| **PUT completed tx case** | Only `procedures` is allowed. You may only **remove** procedures (subset of current IDs or `[]`). You cannot add or swap procedure IDs, or change duration / sequence / order fields. |


### Completed Treatment Case

When the parent tx case is **completed**, the restriction is stricter than “cannot add procedures”:

| Change | Result |
|  --- | --- |
| Any field except `procedures` (duration, sequence, order fields, …) | **400** `"Visit in completed TxCase cannot be updated"` |
| Adding procedures (longer list than current) | **400** `"PatientProcedures cannot be added to the Visit in completed TxCase"` |
| Changing the procedure set (replacements / different IDs) | **400** `"PatientProcedures cannot be updated for the Visit in completed TxCase"` |
| Removing procedures (subset of current IDs, including `[]`) | Allowed (empty list deletes the visit) |


### Delete via Empty Procedures

```
PUT /api/v1/visits/9000000038237
Content-Type: application/json
```

```json
{
  "procedures": []
}
```

Behavior:

- Procedures on the visit are unassigned back to the treatment planner.
- The visit is deleted; sibling visit sequences are reordered.
- If the case has no remaining visits, the **tx case may also be deleted**.
- Linked appointment associations are cleaned up; related appointment “other” status messaging may be updated.
- Successful response returns **`{ "id": <visitId> }`** in `data` (not the full visit resource).


### Side Effects When Changing Procedures

- Procedures are **moved** onto this visit from other visits (or from unassigned). The source visit loses that procedure; if it becomes empty, the source visit is **deleted**.
- Other visits that become empty are deleted; if the case then has no remaining visits, the **tx case may also be deleted**.
- `procedures: []` unassigns procedures back to the treatment planner (they are not deleted).
- Sequence auto-adjusts among sibling visits on the same option on create/update/delete.
- If the visit is linked to an appointment, the response `appointment` field is that linked model; deleting the visit cleans up the association and may update appointment “other” status text.
- Completed, deleted, or voided procedures cannot be moved.


### Example Request — Update Duration and Sequence

```
PUT /api/v1/visits/9000000038237
Content-Type: application/json
```

```json
{
  "duration": 90,
  "sequence": 1
}
```

Returns `200 OK` with the updated `VisitV1` in `data`.

GET — Total Count
— click to expand
```
GET /api/v1/visits/totalcount
```

Uses the **same filter syntax and filter keys** as bulk GET. See **[Bulk GET — Total Count Behavior](/publicapi/bulk-get-total-count)** for how `pagination.total` behaves on bulk GET responses.

### Example Request

```
GET /api/v1/visits/totalcount?filter=lastModified>=2025-01-01,txCase.id==12
```

StreamAPI Webhooks
— click to expand
For real-time visit creates, updates, and deletes, use **StreamAPI webhooks (SNS)** instead of polling bulk GET on a fixed schedule. Setup is covered in **[Creating and Setting Up Webhooks for the StreamAPI](/publicapi/streamapi-webhooks-setup)**.

Visits are streamable under domain type **`VisitV1`**.

## Response Structure

A successful `200`/`201` response returns:

| Field | Type | Description |
|  --- | --- | --- |
| `data` | object or array | Visit record(s), or `{ id }` after delete-via-empty-procedures |
| `warnings` | array | Non-fatal warnings |
| `errors` | array | Field-level or structural errors |


For bulk GET, `data` is an array and includes `meta.pagination`.

### `meta.pagination`

| Field | Type | Description |
|  --- | --- | --- |
| `limit` | integer | Page size used for this request |
| `offset` | integer | Record offset applied |
| `total` | integer | Present only on plain bulk GET without `lastId` or `page` — typically `0` (see [Bulk GET — Total Count Behavior](/publicapi/bulk-get-total-count)) |


### `VisitV1` Object (summary)

| Field | Access | Description |
|  --- | --- | --- |
| `id` | read-only | Unique visit identifier |
| `type` | read-only | Always `"VisitV1"` |
| `txCase` | write on create; read thereafter | Linked treatment case |
| `procedures` | read/write | Linked patient procedures assigned to this visit |
| `duration` | read/write | Length in minutes (`10`–`240`). Omitted on create defaults to `60`; omitted on PUT is unchanged |
| `sequence` | read/write | Order among visits on the tx option |
| `visitProceduresOrderType` | read/write | Procedure sort enum. Optional on create (database default `TEETH`); omitted on PUT is unchanged |
| `descProceduresOrder` | read/write | **Required on POST.** Omitted on PUT is unchanged |
| `appointment` | read-only | Linked appointment when the visit is scheduled (first association) |
| `lastModified` | read-only | Last modification timestamp |


> For the authoritative field list, enums, and validation rules, use the **`VisitV1`** and **`VisitUpdateV1`** schemas in the official API reference.


## Errors & Warnings Reference

HTTP Status Code Errors
— click to expand
These apply to all Visits V1 endpoints.

#### `400` — Bad Request

The request body or query parameters are invalid or cannot be processed. Common causes include invalid `filter` syntax, missing required fields on POST, empty PUT body, empty `procedures` on create, or completed-tx-case update violations.

#### `401` — Unauthorized

|  |  |
|  --- | --- |
| **Message** | Authentication is required and has failed or has not been provided. |
| **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 request is not allowed for this organization context. |
| **Fix** | Confirm the `Organization-ID` header matches the organization on the access token. |


#### `404` — Not Found

|  |  |
|  --- | --- |
| **Message** | `VisitV1 not found` |
| **Why** | No visit exists for the given `visitId` in your organization, or the id is invalid (`0` / non-numeric). |
| **Fix** | Confirm the `visitId` and organization context. |


#### `408` — Request Timeout

|  |  |
|  --- | --- |
| **Message** | The server timed out while processing the request. |
| **Why** | Query too broad — often a wide `lastModified` range without `txCase.id` scoping. |
| **Fix** | Retry with a narrower `filter` or smaller `pageSize`. |


#### `429` — Too Many Requests

|  |  |
|  --- | --- |
| **Message** | Rate limit exceeded. |
| **Fix** | Implement exponential backoff. See the [rate limiting guide](https://papidocs.hs1api.com/publicapi/api-consumer-guide#rate-limiting). |


#### `500` — Internal Server Error

|  |  |
|  --- | --- |
| **Message** | An unexpected error occurred. |
| **Fix** | Retry. If the issue persists, contact support. |


Field-Level Validation
— click to expand
Validation errors are returned in the **`errors`** array on **`400`** responses.

| Message | When |
|  --- | --- |
| `procedures should not be empty` | POST with `procedures: []` |
| `procedures should be an array and not contain null values` | POST with `procedures` omitted or containing `null` |
| `descProceduresOrder should not be empty` / must be a boolean | POST with `descProceduresOrder` omitted or non-boolean |
| `PatientProcedures with ids [...] are completed and cannot be moved` | Moving completed procedures onto a visit |
| `PatientProcedures with ids [...] are in deleted state and cannot be moved` | Moving deleted procedures |
| `PatientProcedures with ids [...] are voided and cannot be moved` | Moving voided procedures |
| `Invalid PatientProcedures ids for particular patient [...]` | Procedure IDs that do not belong to the tx case’s patient |
| `No data for updating is provided` | PUT with an empty body |
| `VisitV1 not found` | GET/PUT `visitId` missing in this organization (**404**) |
| `Visit in completed TxCase cannot be updated` | PUT changing non-`procedures` fields on a completed case |
| `PatientProcedures cannot be added to the Visit in completed TxCase` | PUT adding procedures on a completed case |
| `PatientProcedures cannot be updated for the Visit in completed TxCase` | PUT replacing procedure IDs on a completed case |
| Linked model not found for `txCase` / `procedures` | Invalid or missing linked ids on create/update |
| `duration must not be less than 10` / greater than `240` | Duration out of range (including `0`) |
| `sequence` null / non-integer / negative | Invalid sequence |
| Invalid `visitProceduresOrderType` enum value | Unknown sort enum |