The patient summary over FHIR
Regulation (EU) 2025/327 Annex II 2.1 asks an EHR system for “an interface
enabling access to the personal electronic health data processed by it in
the European electronic health record exchange format”. FerroFED serves that
interface as a FHIR R4 face on a base of its own, {fhir-base}, beside the
ITS-REST face at {base}. No ITS-REST path changes: {base} stays a
conformant ITS-REST surface (Federation Tier §4.1, N1, N28).
The face answers three requests today:
| Request | Answer |
|---|---|
GET {fhir-base}/metadata | the face’s CapabilityStatement |
GET or POST {fhir-base}/Patient/$summary | the patient summary, an HL7 Europe Patient Summary document Bundle |
POST {fhir-base}/Bundle | a document received in the exchange format, written to the member its category is declared for |
The document list, a DocumentReference search that names the summary and
each member’s own document, is planned
(#810). The gate refuses every
other path under {fhir-base}, and every other method, with a 403: a
variant of a path, in another letter case, with a percent-encoded character
or a trailing or doubled slash, is refused, never served.
Asking for a summary
The request is the International Patient Summary 2.0.0 $summary operation
on Patient, at the type level. Name the patient by an identifier with its
system:
GET {fhir-base}/Patient/$summary?identifier=urn:oid:2.999.1|SYNTHETIC-1
Authorization: Bearer <token>
or send the same parameters as a FHIR Parameters body, with
Content-Type: application/fhir+json:
POST {fhir-base}/Patient/$summary
Content-Type: application/fhir+json
{"resourceType":"Parameters","parameter":[
{"name":"identifier","valueString":"urn:oid:2.999.1|SYNTHETIC-1"}]}
identifieris required, once, assystem|value. The system is the namespace the gateway resolves the patient in (§5.2), as it is for an AQL query’sexternal_ref/namespace.profile, when sent, ishttp://hl7.eu/fhir/eps/StructureDefinition/composition-eu-eps, with or without|1.0.0-ballot._format, when sent, isjson,application/jsonorapplication/fhir+json.- Any other parameter is a
400. The face never searches by demographics: a national connector identifies the patient against its own identity services first and asks by the confirmed identifier. - A request by the patient’s logical id,
Patient/{id}/$summary, is refused: the face holds noPatientresource.
What the gateway does
The summary is built from the gateway’s own section queries, the eleven
stored queries under eu.ferrofed.eehrxf
(the gateway’s own queries):
- The header is asked of the demographics binding, the PDQm Supplier of
[pdqm], by the identifier you sent, before any member is asked. The members federate no demographics (Federation Tier §2.3, N32), and nothing of the header reaches a node. - Every section query is read as the façade reads a stored query, the
patient bound through
$patientand$namespacealone. - The patient is localized, checked against the consent pre-filter and resolved once, at every member (§5.2, §14). Each section query is then planned over that one resolution, so the identity services are asked once per summary.
- Every section query goes to every member that holds the patient, scoped
to that member’s own
ehr_id, through the same rewrite and outbound identifier-hygiene gate as any query. No patient identifier reaches a node (§5.4.1, N33). The queries run together under one budget (§11.5). - Each composition a section query selected is mapped by every FHIRconnect mapping the deployment supplies for that section and the composition’s template. A composition no mapping covers is counted in the section’s narrative, never dropped in silence.
- The document is written: the composition, the patient, the gateway’s
Deviceand the operator’sOrganizationas its authors, each member whose data a section holds as an author of that section, oneProvenanceper mapped composition naming the composition and the member, and every mapped resource.
The section queries run from the code that defines them, so the face works
whether or not the deployment sets [stored_queries]. The text is the one
the registry lists at version 1.0.0.
The document
The answer is a Bundle of type document, application/fhir+json, that
claims the bundle-eu-eps profile. Its first entry is the Composition, and
every entry is named under the absolute URL of {fhir-base}.
- The five sections the EPS composition requires are always present: problems, allergies and intolerances, medication summary, procedures, and medical devices. The other six sections the section queries feed follow.
- Every section has a generated narrative that says it is not exhaustive, names every member that gave no answer, and counts what no mapping covers.
- A section with no entry carries the empty reason
nilknownonly when every member answered and none holds content for it, andunavailableotherwise. - Each member’s data are listed as mapped, beside every other member’s, never merged with them.
- The
Patientcarries the identifier you asked by, and the header the demographics binding holds (eHN PS A.1.1, A.1.2): the names, the birth date, the administrative gender, the addresses and the phone numbers and email addresses, each as the PDQm Supplier gives it. An element the Supplier does not hold is left out, never filled in; the birth date, which the EPSPatientrequires, then carries adata-absent-reasonofunknown. The country of affiliation, the preferred professional, the contact person and the insurance (A.1.1.6, A.1.2.2, A.1.2.3, A.1.3) are not in the header.
The tests hold every document the face writes to a structural check against
the vendored bundle-eu-eps, composition-eu-eps and patient-eu-eps
profiles. Validation with HL7’s FHIR Validator in CI is planned
(#688).
Completeness
A summary is all-or-nothing by default, as a federated query is (§11.3,
§11.4). A member that does not answer one of the section queries fails the
summary: the answer is the status the query would take, 424 or 504, with
an OperationOutcome that names each such endpoint and its §11.1 status.
Where the deployment sets federation.best_effort = true, send
openEHR-federation-completeness: partial to take the summary of the members
that answered. Every section then names the members that did not, and none
of them is nilknown.
A patient no member holds is a 404, and so is a patient whose members may
not disclose a restriction, or one the demographics binding does not know:
they answer alike.
The header is asked before any member, and a summary whose header cannot be written asks no member:
| The demographics binding | The answer |
|---|---|
| knows no patient for the identifier | 404 not-found |
| matches several patients | 422 multiple-matches, none of them picked |
| holds the patient with no family name, given name or text | 422 required: the EPS Patient requires a name (ips-pat-1) |
| is not asked about identifiers of the system you sent | 422 not-supported |
| does not answer within its budget | 504 timeout |
| fails, or its exchange cannot be audited | 502 exception |
Authentication and the access log
Every request to the face passes the same client authentication as the
ITS-REST face (§13.1, N25). metadata takes a verified caller. A summary
runs the eleven section queries, so it takes the SMART on openEHR aql-
search permission on each of them: user/aql-*.s, or a pattern that covers
the reserved namespace, such as user/aql-eu.ferrofed.eehrxf::*.s. No
specification defines the scopes of this face: this is our own design until
one does. As for any query that reaches patient data, the caller must state
a purpose of use, a client must act for a professional it names at the
assurance level the issuer requires, and a national contact point’s
connector must relay the attributes its issuer declares. The handler holds
the caller to the scopes the gate found covering the summary once more
before any member is asked.
Every summary that reached a member writes one access record (Annex II 3.2),
naming the verified caller, the patient, every endpoint asked and the
ehr_id at each, and the categories: Patient-Summaries by construction
(Art 14(1)(a)), beside every category the deployment’s map gives the
compositions the members answered with.
Errors
Every error under {fhir-base} is a FHIR OperationOutcome, served as
application/fhir+json. A refusal of the gateway’s own, such as a
client-authentication refusal, keeps its status and its headers, and its
code and message become the issue’s diagnostics:
| Status | Issue type |
|---|---|
400 | invalid |
401 | login |
403 | forbidden |
404 | not-found |
405, 406, 415, 501 | not-supported |
408, 504 | timeout |
424 | incomplete |
429 | throttled |
503 | transient |
| any other | exception |
Receiving a document
Annex II 2.2 and 2.3 ask an EHR system to be able to receive personal
electronic health data in the exchange format. Send a document Bundle to
the face’s Bundle end-point (FHIR R4 documents §3.3.4):
POST {fhir-base}/Bundle
Content-Type: application/fhir+json
Authorization: Bearer <token>
{"resourceType":"Bundle","type":"document", ...}
The gateway stores nothing itself. It writes the document as one openEHR composition to the one member the deployment declares for the document’s category (Federation Tier §2.3, N23):
- The document is read under the FHIR R4 document rules, and its category
is the one whose HL7 Europe document profile fixes a coding of its
Composition.type: LOINC60591-5for a patient summary. A category no member receives is a422that names it. - It is held to the category’s
BundleandCompositionprofiles from the package the deployment supplies. A document that breaks them is a422with one issue per finding, located by element path. A constraint the check cannot evaluate is listed as awarningin the answer, never passed silently. - It is mapped into one composition by the category’s FHIRconnect mapping.
The composition keeps the whole document in its
FEEDER_AUDIT.original_content. - The patient is resolved from every identifier of the document’s
Patientin a namespacefhir.receive_namespacesnames, at the receiving member alone (§5.2). Every one must name the sameehr_idthere. A patient the member holds no EHR for, or identifiers that name two patients, are a422, and nothing is sent to any member. - The composition is committed with ITS-REST
composition_createto the member, located by its ownehr_id. No patient identifier is in the path, the query string or the headers (§5.4.1, N33). The body carries the received document as the clinical content you sent.
A written document answers 200 with an OperationOutcome whose first issue
names the category, the member, the endpoint and the new version’s uid. A
member that refuses the composition is a 502 that names its status, one
that does not answer in time is a 504, and identity services that cannot
answer give a 424. None of them is ever a success.
Receiving takes the SMART on openEHR create permission on compositions of
every template, user/composition-*.c, because the template follows from
the document. A read scope is refused. A patient/ grant, where the issuer
is bound to a member, writes only into its own patient’s EHR. Every receipt
that reached the member writes one access record, an action C with the
document’s category by construction.
Configuring the face
[server]
public_url = "https://gateway.example.org" # the face names its entries under it
[fhir]
base = "/fhir" # {fhir-base}, never {base} or a path under {base}/v1
[fhir.operator]
name = "Example Health Network" # the operator, an author of every summary
identifier_system = "urn:oid:2.999.9" # optional, with identifier_value
identifier_value = "operator-1"
[[fhir.mapping]]
section = "allergies-and-intolerances" # the slug of a section query
template = "/etc/ferrofed/mappings/allergy.opt"
files = [
"/etc/ferrofed/mappings/allergy.yml",
"/etc/ferrofed/mappings/allergy.context.yml",
]
context = "example_allergy.context" # the context mapping's metadata.name
[[fhir.receive]]
category = "Patient-Summaries" # the Art 14(1) category, by its HL7 Europe code
member = "node-a" # the node id of the member that stores it
package = "/etc/ferrofed/hl7.fhir.eu.eps-1.0.0-ballot.tgz"
template = "/etc/ferrofed/mappings/received-summary.opt"
files = [
"/etc/ferrofed/mappings/received-summary.yml",
"/etc/ferrofed/mappings/received-summary.context.yml",
]
context = "example_received_summary.context"
language = "en" # the composition's language, ISO 639-1
territory = "NL" # the composition's territory, ISO 3166-1
For [[fhir.receive]], set receive_namespaces under [fhir] as well,
for example receive_namespaces = ["urn:oid:2.999.1"]: the identifier
systems a received document’s patient is resolved in.
[fhir]needs a demographics binding,[pdqm], because every summary names its patient and the members federate no demographics: configuration load refuses the face without one. The Supplier is asked by the identifier in the system[pdqm.namespaces]maps its namespace to, in the master domain, or in the system you sent when that is an absolute URI.[fhir]needs a registry andserver.public_url, and refuses a base on a path the gateway serves under{base}, or one that overlaps the PMIR feed route (pmir.path).- Each
[[fhir.mapping]]is one FHIRconnect 1.0.0 context mapping, compiled when the configuration is read. It must map to a profile the crosswalk admits for an entry of its section, for exampleallergyIntolerance-eu-corefor the allergies. A mapping that does not compile, or maps elsewhere, refuses the start andferrofed config check. - A section a template feeds through several profiles takes one entry per profile.
- The section slugs are those of the section queries:
allergies-and-intolerances,problems,medication-summary,medical-devices-and-implants,procedures,immunisations,social-history,pregnancy-history,advance-directives,observation-resultsandcare-plans. - Each
[[fhir.receive]]declares one category, once. Its member must be a node of the registry and a resolver must be configured, or the start andferrofed config checkare refused. The package is the vendored HL7 Europe package whose profiles the category’s documents are held to, and the mapping compiles when the configuration is read. - A change to
[fhir]takes a restart.