Retrieve Documents¶
All examples are org-scoped. Replace YOUR_ORG_UUID with your organization's
UUID and YOUR_ACCESS_TOKEN with your bearer token.
List documents¶
curl "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/DocumentReference?_count=100" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Returns a FHIR searchset Bundle, newest first. Combine any of the
parameters below.
| Parameter | Description | Example |
|---|---|---|
date |
Filter by the time Unomed stored the document. Supports FHIR prefixes ge, gt, le, lt, eq; a bare value behaves like eq. Repeat to bound a window. |
ge2026-07-01T00:00:00Z |
_lastUpdated |
Alias for date, same semantics. Supplying both narrows to the tightest window. |
ge2026-07-25T09:00:00Z |
patient |
Match documents for a patient — the Unomed patient UUID, or any external patient identifier the document was tagged with. | 660e8400-… or patient-123 |
status |
current (active) or entered-in-error (soft-deleted). Defaults to all. |
current |
contenttype |
Exact MIME type match (lower-case, no wildcards or partial matches). | application/pdf |
_count |
Max results per page (clamped to 1–200, default 50). | 100 |
_offset |
Pagination offset. | 100 |
date/_lastUpdated filter on the moment the document was stored in Unomed —
not on a clinical date taken from the document's content.
Date precision and prefixes
A date/_lastUpdated value covers the whole period its precision states,
and the prefix applies to that entire period:
| Value | Matches |
|---|---|
ge2026-07-01 |
2026-07-01T00:00:00Z onwards |
le2026-07-25 |
everything up to and including the whole of the 25th |
lt2026-07-25 |
everything before 2026-07-25T00:00:00Z |
gt2026-07-25 |
2026-07-26T00:00:00Z onwards — all of the 25th is excluded |
gt2026-07-25T09:00:00Z |
09:00:01Z onwards — that whole second is excluded |
gt2026-07-25T09:14:32.518412Z |
strictly after that microsecond |
2026-07-25 or eq2026-07-25 |
that whole day |
So ge/le include the stated period and gt/lt exclude all of it. Only
a full fractional-second timestamp gives instant-level precision — that is
the form the API returns. Values may carry an offset (+02:00); they are
normalized to UTC.
Unusable filters are ignored, not rejected
A value the server cannot parse — or a prefix it does not support (ne,
sa, eb, ap) — is dropped silently: the search runs without that
filter and returns the unfiltered result set rather than an error. Timestamps
come back in +00:00 form, so a + in a query string must be
percent-encoded as %2B, or it arrives as a space and the filter is lost.
The simplest way to avoid that is to send the Z form
(2026-07-25T09:14:32.518412Z), and to sanity-check total and link.self
against what you asked for.
The response Bundle¶
Responses are served as application/fhir+json:
{
"resourceType": "Bundle",
"type": "searchset",
"total": 137,
"link": [
{ "relation": "self", "url": "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/DocumentReference?_count=100" },
{ "relation": "next", "url": "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/DocumentReference?_count=100&_offset=100" }
],
"entry": [
{
"fullUrl": "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/DocumentReference/…uuid…",
"resource": {
"resourceType": "DocumentReference",
"id": "…uuid…",
"meta": { "lastUpdated": "2026-07-25T09:14:32.518412+00:00" },
"status": "current",
"date": "2026-07-25T09:14:32.518412+00:00",
"subject": { "reference": "Patient/660e8400-e29b-41d4-a716-446655440001" },
"content": [
{
"attachment": {
"contentType": "application/pdf",
"title": "lab-result.pdf",
"size": 51234,
"hash": "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=",
"url": "https://…s3…/documents/…?X-Amz-Signature=…"
}
}
]
},
"search": { "mode": "match" }
}
]
}
totalis the number of matches across all pages;entry[]is the current page — one entry is shown above, a full page carries up to_countof them. Thenextlink appears only while further pages exist, andpreviousonly when_offsetis greater than 0.dateandmeta.lastUpdatedboth carry the time Unomed stored the document and always hold the same value, written with an explicit UTC offset (+00:00). These are the fields thedate/_lastUpdatedsearch params filter on — use them as your polling cursor (see Incremental polling).subject.referenceisPatient/{id}— the Unomed patient UUID when the document is linked to a patient, otherwise the external identifier it was tagged with. Unmatched documents may have nosubject.content[0].attachment.urlis a presigned download URL valid for one hour.content[0].attachment.hashis the base64-encoded SHA-256 of the file, so you can verify a download end to end;sizeis the length in bytes.
Download the file¶
Follow the presigned attachment.url directly (no auth header needed, valid one
hour):
Or fetch by document id through the authenticated Binary endpoint (useful after the presigned URL has expired):
curl "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/Binary/DOCUMENT_ID" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" -o document.pdf
Incremental polling¶
To pick up only newly-arrived documents, poll on a schedule and advance a cursor based on the document timestamps the API returns — no client clock involved:
- Keep a cursor — the highest
meta.lastUpdated(equivalentlydate) you have processed, and the ids you already filed. Start the cursor at whatever point in history you want to begin from (or omit the filter on the first run to pull everything). -
Query for anything at or after the cursor with the inclusive
geprefix, sending the timestamp inZform: -
Page through
link.nextuntil there is nonext(see Pagination). - File each document you have not seen before, skipping known
resource.ids, then advance the cursor to the largestmeta.lastUpdatedin the results.
Use ge, not gt, and de-duplicate by id
Results are ordered by timestamp alone, with no secondary tiebreaker on id.
If two documents share the same timestamp and only one of them fell on the
page you processed, an exclusive gt<cursor> would skip the other one
permanently. An inclusive ge<cursor> re-returns the documents sitting
exactly on the cursor each time, and de-duplicating by resource.id — which
you need for paging anyway — makes the loop idempotent.
Documents for a single patient¶
When you already know the patient (for example a clinician opened their record),
retrieve everything for that patient in one call with the FHIR
Patient/$everything operation, scoped to documents:
curl "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/Patient/PATIENT_UUID/\$everything?_type=DocumentReference" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Returns the same searchset Bundle shape, newest first, paginated with _count
(1–200, default 50) and _offset. start and end bound the document date
(both inclusive, UTC). Unlike the DocumentReference search, ties on the
timestamp are broken by id, so paging here is deterministic.
A few differences worth knowing:
_typetakes a comma-separated list and defaults to everything this operation aggregates. Omit it and the Bundle also contains the patient'sImagingStudyresources; pass_type=DocumentReferenceto get documents only.- Only documents linked to that Unomed patient record are returned.
A document tagged solely with an external identifier is not included — reach
those with
DocumentReference?patient=your-identifier. - Soft-deleted documents (
status: entered-in-error) are never included. - An unknown
PATIENT_UUID, or one belonging to another organization, returns404.
Resolving the patient¶
If you only hold your own patient identifier (MRN, AHV number, …), resolve it to the Unomed patient UUID first via the Patient search — see Send Patient Data → Search Patients:
curl "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/Patient?identifier=https://your-pis.ch/patients|patient-123" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
The id from the returned Patient is the PATIENT_UUID for $everything, and
matches subject.reference on that patient's documents.
Pagination¶
Results are paged with _count (1–200) and _offset. Prefer following the
Bundle's link entries rather than building offsets yourself:
"link": [
{ "relation": "self", "url": "…?_count=100" },
{ "relation": "next", "url": "…?_count=100&_offset=100" },
{ "relation": "previous", "url": "…?_count=100&_offset=0" }
]
Follow next until it is absent — that is the last page. All search parameters
(including repeated date filters) are preserved on the paging links.
Paging is offset-based over a newest-first result set, so it is a snapshot per request, not a stable server-side cursor:
- Documents arriving while you page shift the following entries to higher offsets, so the same document can show up on two consecutive pages.
- Documents that share a timestamp have no
idtiebreaker, so their relative order between two page requests is not guaranteed.
De-duplicate by resource.id as you go. For large back-fills you can also pin
the window with an upper bound (for example
date=ge2026-07-01&date=lt2026-07-25T00:00:00Z) so incoming documents cannot
disturb the pages you are walking.
Errors¶
| Status | Meaning |
|---|---|
401 |
Missing or invalid access token. |
403 |
Token is not a member of YOUR_ORG_UUID. |
404 |
Unknown document id (DocumentReference/{id}, Binary/{id}) or unknown patient (Patient/{id}/$everything) in this organization. |
200 with total: 0 |
No documents match — an empty Bundle, not an error. |
Search parameters are never rejected: unknown parameters and unparseable values
are ignored rather than answered with a 400, and _count is clamped into
1–200. Verify the echoed link.self if a result set looks larger than expected.