Skip to content

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" }
    }
  ]
}
  • total is the number of matches across all pages; entry[] is the current page — one entry is shown above, a full page carries up to _count of them. The next link appears only while further pages exist, and previous only when _offset is greater than 0.
  • date and meta.lastUpdated both 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 the date / _lastUpdated search params filter on — use them as your polling cursor (see Incremental polling).
  • subject.reference is Patient/{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 no subject.
  • content[0].attachment.url is a presigned download URL valid for one hour.
  • content[0].attachment.hash is the base64-encoded SHA-256 of the file, so you can verify a download end to end; size is the length in bytes.

Download the file

Follow the presigned attachment.url directly (no auth header needed, valid one hour):

curl -L "PRESIGNED_URL_FROM_BUNDLE" -o document.pdf

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:

  1. Keep a cursor — the highest meta.lastUpdated (equivalently date) 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).
  2. Query for anything at or after the cursor with the inclusive ge prefix, sending the timestamp in Z form:

    curl "https://pacs.dev.unomed.ch/YOUR_ORG_UUID/fhir/r4/DocumentReference?date=ge2026-07-25T09:14:32.518412Z&_count=100" \
      -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
    
  3. Page through link.next until there is no next (see Pagination).

  4. File each document you have not seen before, skipping known resource.ids, then advance the cursor to the largest meta.lastUpdated in 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:

  • _type takes a comma-separated list and defaults to everything this operation aggregates. Omit it and the Bundle also contains the patient's ImagingStudy resources; pass _type=DocumentReference to 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, returns 404.

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 id tiebreaker, 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.