OZO FHIR implementation guide
0.9.0 - ci-build
OZO FHIR implementation guide - Local Development build (v0.9.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
The OZO FHIR API defines three CapabilityStatements that together describe the full API contract. All three are available via the /metadata endpoint and in the Artifacts section.
| CapabilityStatement | Description |
|---|---|
| OZO-Server | Server identity, security requirements (Nuts + DPoP + mTLS), and supported OZO profiles. |
| OZO-System | Full CRUD access to all resources. For server-to-server integration (OzoSystemCredential). |
| OZO-Client | Client access for Practitioner, RelatedPerson, and Patient roles. All three roles share the same resource types and interactions — the AAA proxy scopes access differently per role. |
The OZO AAA proxy sits between clients and the FHIR server. It automatically applies search filters to scope results to the authenticated user's access level. Clients do not need to add these filters — the proxy rewrites the query parameters transparently.
For example, when a practitioner searches for GET /fhir/Patient, the proxy rewrites this to:
GET /fhir/Patient?_has:CareTeam:patient:participant={practitionerReference}
This ensures the practitioner only sees patients in their CareTeams, without the client needing to know the filtering logic.
The tables below show the search filters the proxy auto-applies for each role. The {self} placeholder represents the authenticated user's FHIR reference(s).
Authenticated via NutsOrganizationCredential + NutsEmployeeCredential. Access is scoped to CareTeams where the practitioner is a participant.
| Resource | Proxy auto-applied filter |
|---|---|
| Patient | _has:CareTeam:patient:participant={self} |
| Practitioner | _has:CareTeam:participant:participant={self} |
| RelatedPerson | _has:CareTeam:participant:participant={self} |
| CareTeam | participant={self} |
| CommunicationRequest | participant={self OR careTeams} (addressed in recipient or initiating team in extension[senderCareTeam]) |
| Communication | part-of:CommunicationRequest.participant={self OR careTeams} |
| Task | owner={self OR careTeams} (own Tasks and the team Tasks of the practitioner's organizational teams) |
| AuditEvent | agent={careTeamPractitioners} (all practitioners in shared CareTeams) |
| Subscription | criteria rewritten with above filters |
Authenticated via OzoUserCredential. Access is scoped to the related person's own profile and their CareTeam memberships.
| Resource | Proxy auto-applied filter |
|---|---|
| Patient | _has:RelatedPerson:patient:identifier={self} |
| Practitioner | _has:CareTeam:participant:participant={self} |
| RelatedPerson | identifier={self} (own profile only) |
| CareTeam | participant={self} |
| CommunicationRequest | participant={self OR careTeams} |
| Communication | part-of:CommunicationRequest.participant={self OR careTeams} |
| Task | owner={self} |
| AuditEvent | agent={self OR careTeams} |
| Subscription | criteria rewritten with above filters |
Access is scoped to the patient's own record and CareTeams where the patient is the subject.
| Resource | Proxy auto-applied filter |
|---|---|
| Patient | identifier={self} (own record only) |
| Practitioner | _has:CareTeam:participant:patient={self} |
| RelatedPerson | patient={self} |
| CareTeam | patient={self} |
| CommunicationRequest | participant={self OR careTeams} |
| Communication | part-of:CommunicationRequest.participant={self OR careTeams} |
| Task | patient={self} |
| AuditEvent | agent={self OR careTeams} |
| Subscription | criteria rewritten with above filters |
For write operations (POST/PUT), the proxy validates the resource content regardless of role:
| Resource | Validation rule |
|---|---|
| Communication | sender must be the authenticated user; partOf must reference a thread the user is a party of; extension[senderCareTeam], when present, must name an organizational CareTeam of the thread the user participates in. Required from the release after 0.9.0 when the user participates in such a team; 0.9.0 infers it when absent |
| CommunicationRequest | requester must be the authenticated user; extension[senderCareTeam] must be an organizational CareTeam the user participates in and should not be repeated in recipient (validation warning in 0.9.0, error from the next release) |
| AuditEvent | agent[requestor=true].who must be the authenticated user; on a read receipt in a team thread, agent.extension[careTeam] follows the same rule as Communication.extension[senderCareTeam] |
| Subscription | criteria is rewritten to scope results to the user's access |
The IG defines two custom SearchParameters. They are advertised in the OZO-Client and OZO-System CapabilityStatements and installed on the HAPI FHIR server together with the profiles.
| Parameter | Resource | Type | Definition |
|---|---|---|---|
participant |
CommunicationRequest | reference | ozo-communicationrequest-participant: union of recipient and the CareTeam in extension[senderCareTeam], every party of the thread. The proxy scopes thread and message access on it |
sender-careteam |
CommunicationRequest | reference | ozo-communicationrequest-sender-careteam: the CareTeam in extension[senderCareTeam], the team that initiated the thread |
Examples: GET /CommunicationRequest?participant=CareTeam/Pharmacy-A returns every thread Pharmacy A takes part in; GET /CommunicationRequest?sender-careteam=CareTeam/Pharmacy-A returns the threads it initiated. The participant parameter also works chained: GET /Communication?part-of:CommunicationRequest.participant=CareTeam/Pharmacy-A. Existing data must be reindexed once after a parameter is installed; see Installing OZO Package in HAPI FHIR Server.
All CapabilityStatements include Subscription support. Subscriptions use the notify-then-pull pattern required in Dutch healthcare:
The proxy automatically rewrites Subscription criteria to scope notifications to the user's access level, similar to search query rewriting.
Because notifications are empty, the subscriber needs a valid access token to perform the pull — also when no user session is active at that moment. See Background access and subscription-driven pulls for how to keep the practitioner context alive using an IdP refresh token.
See the Individual Messaging and Team-to-Team Messaging pages for detailed Subscription examples and behavior.