# Documents V1 — Developer Guide

> **Reference:** [Documents (V1) Endpoint Documentation](https://papidocs.hs1api.com/publicapi/api-document-management/documents-(v1))


Documents store file content (PDFs, images, Office documents, and more) in Dentrix Ascend. Each document is owned by exactly one of: the **organization**, a **patient**, or a **perio exam**. Tags are optional text labels that can be attached at create or update time.

Supported `mimeType` values: `PDF`, `JPEG`, `PNG`, `GIF`, `DOC`, `DOCX`, `PPT`, `PPTX`, `XLS`, `XLSX`, `MSG`.

> **Filename behavior:** The server stores `name` with a lowercase extension derived from `mimeType` (for example, `"report"` with `mimeType` `"PDF"` becomes `"report.pdf"`). If another document with the same base name already exists for the same owner, the server appends `" (1)"`, `" (2)"`, and so on before the extension.


details
summary
strong
GET — Bulk List Documents
— click to expand
Returns a paginated list of documents in your organization matching the filter criteria.

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


If `filter` is omitted, all documents in the organization are returned (subject to pagination). No automatic date filter is applied.

### 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 | Comma-delimited list of fields to include, or `ALL` |
| `pageSize` | query | No | string | Number of records per page (default 100, maximum 500) |
| `lastId` | query | No | string | Largest document `id` from the previous page — used for cursor-based pagination |


### Filter Keys

| Filter Key | Operators | Example |
|  --- | --- | --- |
| `ownerOrganization.id` | `==` | `?filter=ownerOrganization.id==1006` |
| `ownerPatient.id` | `==` | `?filter=ownerPatient.id==12000006542062` |
| `ownerPerioExam.id` | `==` | `?filter=ownerPerioExam.id==9876543210` |
| `tags` | `->` | `?filter=tags->[Insurance,EOB]` |


> **Note on `ownerOrganization.id`:** Filtering by organization owner matches documents whose owner type is **Organization** only. Documents owned by a patient or perio exam are not included, even when scoped to the same organization.


#### `tags` Operator Details

The `->` operator accepts an array of tag names (IN semantics). Tag names containing commas or special characters can be quoted inside the array.

```
?filter=tags->[macaco,"tag, with, comma"]
```

### Example Requests

List all documents for a patient:

```
GET /api/v1/documents?filter=ownerPatient.id==12000006542062
```

Find documents tagged `Insurance`:

```
GET /api/v1/documents?filter=tags->[Insurance]
```

### Paginating with `lastId`

Use **`lastId`** with **`pageSize`** to page through results. The API returns documents with `id` greater than the cursor value (`id > lastId`), in ascending ID order.

#### Step 1 — Initial Request

Send your first request without `lastId`:

```
GET /api/v1/documents?pageSize=100&filter=ownerPatient.id==12000006542062
```

#### 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/documents?lastId=1000000020702&pageSize=100&filter=ownerPatient.id==12000006542062
```

#### Step 3 — Repeat

Continue until the response returns an empty `data` array.

Use the same `filter` on every page so the cursor stays consistent across the full result set.

details
summary
strong
GET — Document Total Count
— click to expand
Returns the number of documents matching a filter. Uses the same filter syntax as bulk GET.

> **Relationship to bulk GET:** Applying the same `filter` to both endpoints returns a count and records for the same set of documents.


### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `Organization-ID` | header | Yes | string | The organization ID |
| `filter` | query | No | string | Filter criteria (same keys as bulk GET) |


### Filter Keys

Same as [GET — Bulk List Documents](#get-bulk-list-documents): `ownerOrganization.id`, `ownerPatient.id`, `ownerPerioExam.id`, `tags`.

### Example Requests

Count documents for a patient:

```
GET /api/v1/documents/totalcount?filter=ownerPatient.id==12000006542062
```

Count documents with a specific tag:

```
GET /api/v1/documents/totalcount?filter=tags->[Insurance]
```

details
summary
strong
GET — Document by ID
— click to expand
Returns a single document by its ID, including signed URLs in `storage` for downloading file content.

### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `documentId` | path | Yes | integer | The document ID |
| `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/documents/1000000020702
```

details
summary
strong
POST — Create Document
— click to expand
Creates a new document with file content supplied as base64 in the request body. Returns **201** with the created document in `data`.

All creates require `name`, `mimeType`, and `file.content`. Provide **at most one** owner field (`ownerOrganization`, `ownerPatient`, or `ownerPerioExam`). If none is provided, the document is assigned to the requesting organization.

> **MIME alias:** `JPG`, `JPE`, `JIF`, `JFIF`, and `JFI` are normalized to `JPEG`.


details
summary
strong
Scenario 1 — Organization-Owned Document (Default)
— click to collapse
Creates a document owned by the organization. Omit owner fields to use this default.

#### Required Fields

| Field | Type | Description |
|  --- | --- | --- |
| `name` | string | Base filename, 1–70 characters (extension is appended by the server) |
| `mimeType` | string | One of the supported MIME types |
| `file.content` | string | Base64-encoded file content |


#### Example Request Body

```json
{
  "name": "office-policy",
  "mimeType": "PDF",
  "file": {
    "content": "JVBERi0xLjQKJeLjz9MKMy..."
  }
}
```

#### Example Response (`name` in `data`)

```json
{
  "data": {
    "id": 1000000020702,
    "name": "office-policy.pdf",
    "mimeType": "PDF",
    "ownerType": "Organization",
    "ownerOrganization": { "id": "1006" },
    "storage": { "original": "https://..." }
  }
}
```

details
summary
strong
Scenario 2 — Patient-Owned Document
— click to expand
Attaches a document to a specific patient.

#### Required Fields

Same as Scenario 1, plus:

| Field | Type | Description |
|  --- | --- | --- |
| `ownerPatient` | object | `{ "id": <patientId> }` |


#### Example Request Body

```json
{
  "name": "consent-form",
  "mimeType": "PDF",
  "ownerPatient": { "id": 12000006542062 },
  "file": {
    "content": "JVBERi0xLjQKJeLjz9MKMy..."
  }
}
```

details
summary
strong
Scenario 3 — Perio-Exam-Owned Document
— click to expand
Attaches a document to a specific perio exam.

#### Required Fields

Same as Scenario 1, plus:

| Field | Type | Description |
|  --- | --- | --- |
| `ownerPerioExam` | object | `{ "id": <perioExamId> }` |


#### Example Request Body

```json
{
  "name": "perio-chart",
  "mimeType": "PDF",
  "ownerPerioExam": { "id": 9876543210 },
  "file": {
    "content": "JVBERi0xLjQKJeLjz9MKMy..."
  }
}
```

details
summary
strong
Scenario 4 — Image Upload (Optional Crop/Resize)
— click to expand
For `JPEG`, `PNG`, or `GIF`, you may include optional dimension fields on `file` to generate thumbnail, resized, and cropped variants in `storage`. When dimension fields are omitted, only the original file is stored.

| Field | Type | Description |
|  --- | --- | --- |
| `file.fromX` | integer | Crop origin X (optional) |
| `file.fromY` | integer | Crop origin Y (optional) |
| `file.width` | integer | Target width (optional) |
| `file.height` | integer | Target height (optional) |
| `file.ratio` | number | Scale ratio applied to width/height (optional) |


All five dimension fields must be present for crop/resize processing to run.

#### Example Request Body

```json
{
  "name": "xray",
  "mimeType": "JPEG",
  "ownerPatient": { "id": 12000006542062 },
  "file": {
    "content": "/9j/4AAQSkZJRgABAQAA...",
    "fromX": 0,
    "fromY": 0,
    "width": 800,
    "height": 600,
    "ratio": 1.0
  }
}
```

Image responses may include `storage.original`, `storage.thumbnail`, `storage.resized`, and `storage.cropped` when processing runs.

details
summary
strong
Scenario 5 — Document with Tags
— click to expand
Tags can be supplied on create. Each tag is a string, 1–50 characters. New tag names are created automatically; existing names are reused (case-insensitive).

#### Example Request Body

```json
{
  "name": "insurance-eob",
  "mimeType": "PDF",
  "ownerPatient": { "id": 12000006542062 },
  "tags": ["Insurance", "EOB"],
  "file": {
    "content": "JVBERi0xLjQKJeLjz9MKMy..."
  }
}
```

details
summary
strong
PUT — Update Document
— click to expand
Updates an existing document. Only `name` and `tags` can be changed; file content, `mimeType`, and owner cannot be updated via this endpoint.

Returns **200** with the updated document in `data`. The server appends the file extension to `name` on update (same as create).

### Parameters

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


### Updatable Fields

| Field | Type | Description |
|  --- | --- | --- |
| `name` | string | New base filename, 1–70 characters |
| `tags` | array | Replaces all tags on the document. Pass `[]` to remove all tags. |


### Example Request

```
PUT /api/v1/documents/1000000020702
```

```json
{
  "name": "updated-consent",
  "tags": ["Legal", "Signed"]
}
```

details
summary
strong
DELETE — Delete Document
— click to expand
Permanently deletes a document, its stored file content, and tag associations. Unreferenced tags are removed. If this was the last document for an owner, the owner record is also removed.

Returns **200** with the deleted document ID in `data`.

### Parameters

| Parameter | In | Required | Type | Description |
|  --- | --- | --- | --- | --- |
| `documentId` | path | Yes | integer | The document ID to delete |
| `Organization-ID` | header | Yes | string | The organization ID |


### Example Request

```
DELETE /api/v1/documents/1000000020702
```

#### Example Response

```json
{
  "data": {
    "id": "1000000020702"
  }
}
```

details
summary
strong
Streaming
— click to expand
Documents are streamable via the [Streaming API](https://papidocs.hs1api.com/publicapi/api-consumer-guide#streaming). The domain type is **`DocumentV1`**.

Tag changes are reflected through document CREATE, UPDATE, and DELETE events — there is no separate tag stream domain.

### Routing Key Format

```
organizationId.locationId.DocumentV1.operationType
```

### Example Routing Keys

Listen for all document events across all locations:

```
66d22762060811049b50d085.*.DocumentV1.*
```

Listen for creates only:

```
66d22762060811049b50d085.*.DocumentV1.CREATE
```

Listen for deletes at a specific location:

```
66d22762060811049b50d085.1000000000123.DocumentV1.DELETE
```

### Operations Emitted

| Operation | When |
|  --- | --- |
| `CREATE` | When a new document is created (POST) |
| `UPDATE` | When a document name or tags are changed (PUT) |
| `DELETE` | When a document is deleted (DELETE) |


## Response Structure

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

| Field | Type | Description |
|  --- | --- | --- |
| `data` | object or array | The created, retrieved, or updated document(s) |
| `warnings` | array | Non-fatal warnings |
| `errors` | array | Field-level errors |


For bulk GET, `data` is an array and includes `meta.pagination`. For total count, `data` contains `totalCount`.

### `meta.pagination` Object (bulk GET)

| Field | Type | Description |
|  --- | --- | --- |
| `limit` | integer | Page size applied to the request (from `pageSize`, or default 100) |
| `offset` | integer | Offset into the result set (0 when using `lastId` from the first page) |
| `total` | integer | Total matching records when available; may be `0` when not computed |


When paging with `lastId`, read the **`id`** of the last item in `data` and pass it as the `lastId` query parameter on the next request.

### Total Count `data` Object

| Field | Type | Description |
|  --- | --- | --- |
| `totalCount` | integer | Number of documents matching the filter |


### `data` Object — Document Fields

| Field | Type | Read-Only | Description |
|  --- | --- | --- | --- |
| `id` | integer | Yes | Unique document ID |
| `name` | string | No | Stored filename including extension |
| `mimeType` | string | No | File type enum value |
| `guidName` | string | Yes | Server-assigned storage identifier |
| `thumbNailId` | string | Yes | Thumbnail storage identifier (image documents) |
| `ownerType` | string | Yes | `Organization`, `Patient`, or `PerioExam` |
| `ownerOrganization` | object | No | `{ "id": ... }` when `ownerType` is Organization |
| `ownerPatient` | object | No | `{ "id": ... }` when `ownerType` is Patient |
| `ownerPerioExam` | object | No | `{ "id": ... }` when `ownerType` is PerioExam |
| `tags` | array | No | Tag name strings attached to the document |
| `userDefinedDate` | string | No | User-assigned date (`YYYY-MM-DD`) |
| `storage` | object | Yes | Signed download URLs (see below) |
| `type` | string | Yes | Always `DocumentV1` |


#### `storage` Object

| Field | Present When | Description |
|  --- | --- | --- |
| `original` | Always (after create) | Signed URL for the uploaded file |
| `thumbnail` | Image with processing | Signed URL for thumbnail |
| `resized` | Image with crop/resize | Signed URL for resized variant |
| `cropped` | Image with crop/resize | Signed URL for cropped variant |


Non-image documents (for example PDF) typically return only `storage.original`.

## Errors & Warnings Reference

details
summary
strong
HTTP Status Code Errors
— click to expand
#### `400` — Bad Request

The request body or parameters are invalid. See field-level errors below.

#### `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. |
| **Fix** | Confirm credentials and organization access with your integration administrator. |


#### `404` — Not Found

|  |  |
|  --- | --- |
| **Message** | The requested resource is either missing or does not exist. |
| **Why** | No document exists for the given `documentId` within your organization, or the path ID is not a valid numeric ID. |
| **Fix** | Confirm the `documentId` is correct and belongs to your organization. |


#### `408` — Request Timeout

|  |  |
|  --- | --- |
| **Message** | The server timed out while processing the request. |
| **Fix** | Retry the request. Narrow filter criteria on bulk requests if timeouts persist. |


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


details
summary
strong
Field-Level Validation Errors
— click to collapse
### `name`

**Error:** Missing or invalid `name`.

|  |  |
|  --- | --- |
| **Why** | `name` was not provided, is empty, or exceeds 70 characters. |
| **Fix** | Provide a string between 1 and 70 characters (extension is added by the server). |


### `mimeType`

**Error:** Invalid `mimeType`.

|  |  |
|  --- | --- |
| **Why** | The value is not one of the supported MIME type enum values. |
| **Fix** | Use a supported value: `PDF`, `JPEG`, `PNG`, `GIF`, `DOC`, `DOCX`, `PPT`, `PPTX`, `XLS`, `XLSX`, or `MSG`. |


### `file.content`

**Error:** Missing or invalid file content.

|  |  |
|  --- | --- |
| **Why** | `file.content` was omitted, empty, or not valid base64. |
| **Fix** | Provide base64-encoded file bytes in `file.content`. |


### Owner fields

**Error:** Multiple or invalid owner fields.

|  |  |
|  --- | --- |
| **Why** | More than one of `ownerOrganization`, `ownerPatient`, and `ownerPerioExam` was supplied; `ownerOrganization.id` does not match the request organization; or a linked patient/perio exam ID is invalid. |
| **Fix** | Provide at most one owner field. Use a valid patient or perio exam ID in your organization, or omit owner fields to default to the organization. |


### `tags`

**Error:** Invalid tag value.

|  |  |
|  --- | --- |
| **Why** | A tag string is empty or exceeds 50 characters. |
| **Fix** | Each tag must be 1–50 characters. |


### `filter`

**Error:** Invalid filter.

|  |  |
|  --- | --- |
| **Why** | An unsupported filter key, operator, or value was used (for example a non-numeric owner ID). |
| **Fix** | Use the documented filter keys and operators. See [Filter Keys](#filter-keys). |