Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

RequestAnswer
GET {fhir-base}/metadatathe face’s CapabilityStatement
GET or POST {fhir-base}/Patient/$summarythe patient summary, an HL7 Europe Patient Summary document Bundle
POST {fhir-base}/Bundlea 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"}]}
  • identifier is required, once, as system|value. The system is the namespace the gateway resolves the patient in (§5.2), as it is for an AQL query’s external_ref/namespace.
  • profile, when sent, is http://hl7.eu/fhir/eps/StructureDefinition/composition-eu-eps, with or without |1.0.0-ballot.
  • _format, when sent, is json, application/json or application/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 no Patient resource.

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):

  1. 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.
  2. Every section query is read as the façade reads a stored query, the patient bound through $patient and $namespace alone.
  3. 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.
  4. 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).
  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.
  6. The document is written: the composition, the patient, the gateway’s Device and the operator’s Organization as its authors, each member whose data a section holds as an author of that section, one Provenance per 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 nilknown only when every member answered and none holds content for it, and unavailable otherwise.
  • Each member’s data are listed as mapped, beside every other member’s, never merged with them.
  • The Patient carries 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 EPS Patient requires, then carries a data-absent-reason of unknown. 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 bindingThe answer
knows no patient for the identifier404 not-found
matches several patients422 multiple-matches, none of them picked
holds the patient with no family name, given name or text422 required: the EPS Patient requires a name (ips-pat-1)
is not asked about identifiers of the system you sent422 not-supported
does not answer within its budget504 timeout
fails, or its exchange cannot be audited502 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:

StatusIssue type
400invalid
401login
403forbidden
404not-found
405, 406, 415, 501not-supported
408, 504timeout
424incomplete
429throttled
503transient
any otherexception

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):

  1. 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: LOINC 60591-5 for a patient summary. A category no member receives is a 422 that names it.
  2. It is held to the category’s Bundle and Composition profiles from the package the deployment supplies. A document that breaks them is a 422 with one issue per finding, located by element path. A constraint the check cannot evaluate is listed as a warning in the answer, never passed silently.
  3. It is mapped into one composition by the category’s FHIRconnect mapping. The composition keeps the whole document in its FEEDER_AUDIT.original_content.
  4. The patient is resolved from every identifier of the document’s Patient in a namespace fhir.receive_namespaces names, at the receiving member alone (§5.2). Every one must name the same ehr_id there. A patient the member holds no EHR for, or identifiers that name two patients, are a 422, and nothing is sent to any member.
  5. The composition is committed with ITS-REST composition_create to the member, located by its own ehr_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 and server.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 example allergyIntolerance-eu-core for the allergies. A mapping that does not compile, or maps elsewhere, refuses the start and ferrofed 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-results and care-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 and ferrofed config check are 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.