Public Patient Documents API
Purpose
This page explains how an external client can:
- Create a patient document placeholder in the root directory or a folder path.
- Upload the file content to the returned storage URL.
- Search for documents.
- Traverse a patient's documents by path.
The API specification defines the endpoint contracts, request fields, and response schemas. This guide complements it with an end-to-end integration workflow using the external /v1 routes.
Prerequisites
Every Patient Documents API request requires:
- An OAuth 2.0 bearer token.
- The UUID of the customer organization in the
Organization-IDheader. - Access to the patient identified by
patientId. - Patient consent for the organization.
Patient.File.Writepermission for document creation.Patient.File.Readpermission for document search.
Create a folder and file placeholder
This operation creates a document placeholder. File bytes are uploaded separately using the returned storageUrl.
There is no separate public Create Folder endpoint. Folders are expressed through the document's path. Supplying a nested path places the placeholder in that folder hierarchy in file storage.
Create a file at the patient root
Request:
curl --request POST \
--url "$BASE_URL/v1/patients/$PATIENT_ID/documents" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Organization-ID: $ORGANIZATION_ID" \
--header "Content-Type: application/json" \
--data '{
"fileName": "consent-form.pdf"
}'
Create a nested folder path and file
The following request places the document under /Cases/2026/Initial-Scan:
curl --request POST \
--url "$BASE_URL/v1/patients/$PATIENT_ID/documents" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Organization-ID: $ORGANIZATION_ID" \
--header "Content-Type: application/json" \
--data '{
"fileName": "upper-jaw.stl",
"path": "/Cases/2026/Initial-Scan"
}'
Successful response: 201 Created
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"storageUrl": "https://storage.example.com/patient-documents/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"path": "/Cases/2026/Initial-Scan"
}
Upload the file content
Document creation and content upload are separate steps.
Current AXS web-client behavior
The current AXS frontend uploads content with an authenticated HTTP POST to {storageUrl}/file. The request requires the OAuth bearer token in the Authorization header and sends multipart form data with these fields:
| Field | Value |
|---|---|
file | Binary file content |
FileName | Original file name |
Type | Uppercase file type, such as PDF or STL |
Id | id returned by Create Document |
Example matching the current frontend implementation:
DOCUMENT_ID="3fa85f64-5717-4562-b3fc-2c963f66afa6"
STORAGE_URL="<storageUrl returned by Create Document>"
curl --request POST \
--url "$STORAGE_URL/file" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--form "file=@./upper-jaw.stl" \
--form "FileName=upper-jaw.stl" \
--form "Type=STL" \
--form "Id=$DOCUMENT_ID"
Do not set the multipart Content-Type header manually; the HTTP client must add the boundary. Confirm the storage contract before using this implementation outside the AXS web client because the Developer Portal does not guarantee these multipart details.
Search documents
The API specification contains the complete search request contract. The examples below focus on common integration workflows. Always provide offset and limit for predictable pagination.
Search all uploaded documents
Because public searches include nested paths, searching / returns files recursively from the patient root and all descendant paths.
curl --request POST \
--url "$BASE_URL/v1/patients/$PATIENT_ID/documents/search" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Organization-ID: $ORGANIZATION_ID" \
--header "Content-Type: application/json" \
--data '{
"offset": 0,
"limit": 30,
"path": "/",
"hasContent": true,
"sortBy": "lastModifiedAt",
"sortDirection": "DESC"
}'
Example response:
{
"documents": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"storageUrl": "https://storage.example.com/patient-documents/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"fileName": "upper-jaw.stl",
"fileType": "STL",
"lastModifiedAt": "2026-08-11T09:30:00Z",
"fileSize": 204800,
"storageDownloadUri": "https://storage.example.com/download/upper-jaw.stl?signature=abc123",
"thumbnailDownloadUri": null,
"path": "/Cases/2026/Initial-Scan"
}
],
"offset": 0,
"limit": 30,
"total": 1
}
storageDownloadUri and thumbnailDownloadUri are time-limited. A thumbnail can be null when the file type does not support previews.
Search within a folder subtree
This search returns files in /Cases/2026 and its descendant paths, including /Cases/2026/Initial-Scan:
curl --request POST \
--url "$BASE_URL/v1/patients/$PATIENT_ID/documents/search" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Organization-ID: $ORGANIZATION_ID" \
--header "Content-Type: application/json" \
--data '{
"offset": 0,
"limit": 30,
"path": "/Cases/2026",
"hasContent": true,
"sortBy": "fileName",
"sortDirection": "ASC"
}'
Search by filename and type
{
"offset": 0,
"limit": 30,
"fileName": "upper-jaw",
"fileType": "STL",
"hasContent": true
}
Find specific documents by ID
{
"offset": 0,
"limit": 30,
"id": [
"3fa85f64-5717-4562-b3fc-2c963f66afa6",
"24a92682-69a9-4cd5-8100-cf1b25c1fd8f"
]
}
When id contains values, ID lookup takes precedence over filename, path, content-status, and file-type filters.
Traverse folders and files
The public API returns files, not folder nodes. A result has a path, but it does not have isFolder, child collections, or a folder ID. Empty folders are therefore not discoverable through the public search API.
Use path-based traversal:
- Search
path: "/"to obtain all files recursively. - Build the visible folder tree by grouping documents by their
pathvalues. - Search a selected path to load only that subtree.
- Continue paging until all records represented by
totalhave been read.
For example, these results:
[
{ "fileName": "consent-form.pdf", "path": "/" },
{ "fileName": "upper-jaw.stl", "path": "/Cases/2026/Initial-Scan" },
{ "fileName": "lower-jaw.stl", "path": "/Cases/2026/Initial-Scan" },
{ "fileName": "treatment-plan.pdf", "path": "/Cases/2026/Planning" }
]
can be presented as:
/
|-- consent-form.pdf
`-- Cases
`-- 2026
|-- Initial-Scan
| |-- upper-jaw.stl
| `-- lower-jaw.stl
`-- Planning
`-- treatment-plan.pdf
Searching /Cases/2026/Initial-Scan returns both jaw scans. Searching /Cases/2026 returns files from both Initial-Scan and Planning because nested search is enabled by the public API.
Pagination example
The response total is the total number of matching documents, not the number in the current page.
offset = 0
limit = 30
repeat:
response = search(offset, limit, path)
process response.documents
offset = offset + response.documents.length
until offset >= response.total or response.documents is empty
For stable traversal across multiple pages, specify the same sortBy and sortDirection on every request.
Verify a completed upload
After uploading the bytes, search by the returned document ID with hasContent: true:
curl --request POST \
--url "$BASE_URL/v1/patients/$PATIENT_ID/documents/search" \
--header "Authorization: Bearer $ACCESS_TOKEN" \
--header "Organization-ID: $ORGANIZATION_ID" \
--header "Content-Type: application/json" \
--data "{
\"offset\": 0,
\"limit\": 1,
\"id\": [\"$DOCUMENT_ID\"],
\"hasContent\": true
}"
A matching result confirms that the document is searchable. Check fileSize and storageDownloadUri before treating the upload as complete.
Error handling
Public API errors use a problem-details response containing fields such as type, title, status, detail, instance, messageKey, and occurredAt.
| Status | Meaning | Typical action |
|---|---|---|
400 Bad Request | Invalid UUID, blank filename, invalid path, invalid pagination, or more than 30 IDs. | Correct the request; do not retry unchanged. |
401 Unauthorized | Missing, invalid, or expired access token. | Obtain a valid token and retry. |
403 Forbidden | Missing scope, organization access, patient access, or consent. | Correct authorization or consent before retrying. |
404 Not Found | Patient or requested document does not exist. | Verify identifiers and region. |
409 Conflict | A duplicate document or conflicting storage operation exists. | Search before retrying or choose another filename/path. |
413 Payload Too Large | Request, header, path, or storage upload exceeds a configured limit. | Reduce the input or consult the storage limits. |
500 Internal Server Error | Unexpected service or storage failure. | Retry with backoff and retain the problem details for support. |
Example:
{
"type": "https://docs.straumann.com/errors/validation",
"title": "Bad Request",
"status": 400,
"detail": "Validation failed for field 'fileName': must not be blank",
"instance": "/public/v1/patients/123e4567-e89b-12d3-a456-426614174000/documents",
"messageKey": "VALIDATION_ERROR",
"occurredAt": "2026-08-11T12:36:06.152Z"
}
The instance field can contain a /public/v1 path even though external clients call /v1.
Integration checklist
- Select the correct regional pre-production or production base URL.
- Acquire a token with the required Patient File scope.
- Send
Organization-IDon both create and search requests. - Create the placeholder before sending file bytes.
- Confirm the separate storage upload contract for non-AXS clients.
- Use
hasContent: truewhen only completed uploads should be shown. - Send an explicit
offset,limit, and stable sort on every search. - Traverse by
path; do not expect folder objects from the public search response. - Use
storageDownloadUri, notstorageUrl, to download file content.