Huli FHIR Core Implementation Guide (R4)
0.1.0 - Release
Huli FHIR Core Implementation Guide (R4) - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
HuliDocumentReference points at an uploaded patient document — a lab result,
imaging study, or scanned file (ehr_document_reference). The Public API
supports read, search, create, and a dedicated $upload operation; a metadata
PUT (and the entered-in-error soft-delete) round out the surface. The binary
itself is served via a short-lived signed URL, never inlined on read.
This resource is a thin FHIR layer over the same practice document service the in-app document manager uses, so every write inherits the application's upload rules: the 25 MB ceiling, magic-byte content validation, SHA-256 dedup, and organization scoping.
Fail-closed when storage is unconfigured. When the server has no document storage (GCS bucket) wired, every DocumentReference route returns a
503 Service UnavailableOperationOutcomerather than partially serving.
Every operation requires the SMART scope system/DocumentReference.rs (read /
search) or system/DocumentReference.cru (create / read / update / $upload)
AND the corresponding practice document permission on the api key's user. A valid
scope on a key whose user lacks the permission is denied with 403.
DocumentReference is clinical-sensitive and therefore BAA-gated: an api
key without an executed Business Associate Agreement cannot obtain these scopes
(it is part of the "Medical records" scope card, alongside Composition). See
Data handling and compliance for the
integrator BAA obligations.
Huli field (ehr_document_reference) |
FHIR path | Cardinality | Must Support | Notes |
|---|---|---|---|---|
id_document |
DocumentReference.id |
1..1 | – | UUID, allocated server-side; client-supplied IDs are rejected |
modified_on |
DocumentReference.meta.lastUpdated / meta.versionId |
0..1 | – | versionId derived from modified_on (no conditional update on this surface) |
| – | DocumentReference.meta.profile[0] |
0..1 | – | Always https://fhir.huli.ai/r4/StructureDefinition/HuliDocumentReference |
status + deleted_on |
DocumentReference.status |
1..1 | Yes | current while active (status = 1); entered-in-error once soft-deleted |
id_patient |
DocumentReference.subject |
1..1 | Yes | Reference(HuliPatient); required on read and write |
id_practitioner |
DocumentReference.author[0] |
0..* | Yes | Reference(HuliPractitioner); the practitioner the document is routed to for review — set on write (inline author[0] / multipart reviewer), defaults to the organization owner |
id_encounter |
DocumentReference.context.encounter[0] |
0..1 | – | Reference(HuliEncounter); the clinical encounter the document is bound to |
category |
DocumentReference.category[0].coding[0] |
0..* | – | system = https://fhir.huli.ai/r4/CodeSystem/document-category, code = the category code |
document_type_code / _display |
DocumentReference.type |
0..1 | – | LOINC-coded document type (system = http://loinc.org) |
description |
DocumentReference.description |
0..1 | – | Free-text description |
created_on |
DocumentReference.date |
0..1 | – | When the document record was created |
content_type |
content[0].attachment.contentType |
1..1 | Yes | The server-detected MIME type (see Content validation) |
storage_path |
content[0].attachment.url |
0..1 | Yes | A 30-minute signed GCS URL (read only); never inlined; absent on search rows |
file_size |
content[0].attachment.size |
0..1 | – | Byte size |
file_hash |
content[0].attachment.hash |
0..1 | – | SHA-256 digest; stored as hex, emitted base64-encoded per FHIR |
filename |
content[0].attachment.title |
0..1 | – | Original filename |
document_date |
content[0].attachment.creation |
0..1 | – | The clinical date of the document content (YYYY-MM-DD), when recorded |
Huli stores exactly one binary per document, so content is 1..1 and
content[0].attachment is always present.
FHIR DocumentReference.status |
Huli concept | Notes |
|---|---|---|
current |
active row (status = 1) |
Default for an uploaded document |
entered-in-error |
soft-deleted (deleted_on set) |
Reached via PUT status=entered-in-error |
status is required on write. superseded has no Huli equivalent
(documents are not versioned on this surface) and is rejected 422 with a
value issue at DocumentReference.status — only current and
entered-in-error are honored.
A voided (entered-in-error) document stays retrievable as a tombstone: a
GET by id returns 200 with status=entered-in-error and the stable metadata,
but no content.attachment.url — the binary is gone, so no download link is
served. The tombstone also appears in Patient/$everything (consistent with a
voided Composition). Search (GET /DocumentReference?…) is active-document
discovery and does not return voided documents — reach them by id or via
$everything.
A document binary can be created two ways. Both route through the practice
document service's Upload, inheriting every upload guard below.
POST /fhir/R4/DocumentReference (inline base64)The body is a DocumentReference carrying the binary inline as
content[0].attachment.data (base64). Required: subject (Patient reference),
content[0].attachment.data, and content[0].attachment.title (the filename —
its extension must match the detected content type). Optional:
context.encounter[0] and author[0] (the reviewer — see
Review assignment).
{
"resourceType": "DocumentReference",
"status": "current",
"subject": { "reference": "Patient/01936b7c-8d4e-7000-8000-000000004001" },
"author": [{ "reference": "Practitioner/01936b7c-8d4e-7000-8000-000000003001" }],
"context": { "encounter": [{ "reference": "Encounter/01936b7c-8d4e-7000-8000-000000005001" }] },
"content": [
{
"attachment": {
"contentType": "application/pdf",
"title": "resultado-laboratorio.pdf",
"data": "JVBERi0xLjQKJ..."
}
}
]
}
POST /fhir/R4/DocumentReference/$uploadThe $upload operation accepts two modes:
multipart/form-data — a file part (the raw binary) plus a subject
form field (a Patient reference or bare UUID) and optional encounter,
category, and reviewer fields. This is the preferred path for large
binaries — the stream is bounded as it is read.POST /DocumentReference above
(content[0].attachment.data base64).A successful create returns 201 Created with a Location header and the
rendered DocumentReference (the response carries a freshly minted 30-minute
signed content[0].attachment.url).
Every uploaded document is routed to one practitioner's review queue. Name that
practitioner with author[0] (inline mode) or the reviewer form field
(multipart) — a Practitioner reference or bare UUID that must resolve to an
active member of the caller's organization (an unknown or non-member reviewer is
rejected 400). When none is supplied the document defaults to the
organization's owner.
The binary may not exceed 25 MB. An oversized upload is rejected 413
(HPB-00119) — the inline base64 path reads under a ceiling sized for the ~4/3
base64 inflation plus a JSON-envelope margin, and the multipart path bounds the
stream as it is read so an oversized body is never fully buffered first.
The content type is detected from magic bytes — the client-declared
attachment.contentType is informational and does not gate the upload. The
allowed types are PDF, JPEG, PNG, WebP, and DICOM; anything else is rejected
400 (HPB-00120). The supplied filename's extension must match the
detected type (e.g. a PDF magic header with a .png filename is rejected
400). The stored content[0].attachment.contentType reflects the
server-detected type, not the client's claim.
The service computes a SHA-256 of the bytes and checks for a byte-identical
existing document in the organization. A duplicate is flagged (recorded in the
document's metadata), not rejected — the upload still succeeds and returns a
new DocumentReference.
PUT /fhir/R4/DocumentReference/{id} mutates metadata only — the binary
content is immutable on this surface:
| Body shape | Effect |
|---|---|
status = current + category / description |
Updates the document category and/or description (UpdateDocumentMetadata) |
status = entered-in-error |
Soft-deletes the document (DeleteDocument); the response renders the now entered-in-error resource |
There is no hard DELETE verb — soft-delete is reached exclusively through
PUT status=entered-in-error. A binary re-upload is a new $upload, never a
content PUT.
GET /fhir/R4/DocumentReference requires the patient parameter (the practice
document store exposes only patient-scoped reads):
| Parameter | Type | Notes |
|---|---|---|
patient |
reference | Required. Reference(HuliPatient); logical id or full reference |
category |
token | The document-category code; pushed down to the store (exact total) |
type |
token | The LOINC document-type code; best-effort filter over the current page |
date |
date | The document creation date; best-effort filter over the current page |
_count |
number | Page size |
_cursor |
string | Opaque continuation cursor from the previous page's next link |
v1 search limitation.
categoryis pushed down to the store and paginates with an exactBundle.total.typeanddateare applied as best-effort filters over the already-fetched page — they do not repaginate, so a page may return fewer than_countmatches even when more exist further down. Search rows carry no signed URL; fetch the individualDocumentReference/{id}to obtain the binary's download URL.
Search responses are returned as a FHIR Bundle of type searchset.
_history / vread. The DocumentReference exposes only its current
state.content[0].attachment.data is empty
on read; the bytes are reachable only through the 30-minute signed
content[0].attachment.url, which the client must use before it expires.| Code | HTTP | When |
|---|---|---|
HPB-00118 |
404 | DocumentReference not found in the authenticated organization |
HPB-00119 |
413 | Upload exceeds the 25 MB ceiling |
HPB-00120 |
400 | Content type not allowed, or the declared/extension type does not match the detected bytes |
A 503 Service Unavailable is returned when document storage is unconfigured on
the server. All errors are returned as a FHIR OperationOutcome.