OZO FHIR implementation guide
0.8.4 - ci-build
OZO FHIR implementation guide - Local Development build (v0.8.4) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions
Changes to this page per IG version. The full project history is on the Changelog page.
| Version | Date | Change |
|---|---|---|
| 0.8.4 | 2026-10-01 | Subscription criteria Task?id, Communication?id and CommunicationRequest?id replaced by Task?, Communication? and CommunicationRequest?. id is not a search parameter (the resource id parameter is _id); HAPI only accepted the old form because it drops parameters without a value. Note on the criteria form added. The team messaging model itself is under review; see RFC - Team Messaging Model. |
| 0.8.3 | 2026-10-01 | Sender's team tightened to the organizational recipient teams (no subject) that list the sender as participant; a patient care team never counts, so team-wide read never applies to a patient network. extension[senderCareTeam] must be an organizational team the requester participates in. inResponseTo marked as optional in the reply steps. It is a quote-reply link between messages; the thread link is partOf and the OZO FHIR Api does not use inResponseTo. Read receipt: two entity entries instead of "entity.what has two values"; the CommunicationRequest entity is required, the Communication entity optional. Note added that "mark as unread" is not part of the model. |
| 0.8.0 | 2026-09-17 | CommunicationRequest.recipient lists both teams, including the initiating team from extension[senderCareTeam] (invariant ozo-cr-sender-careteam-in-recipient). Task handling described per sender's team (team-wide read). Read receipts use AuditEvent type access (system iso-21089-lifecycle). Query patterns use part-of instead of based-on; new sender-careteam search parameter. Sequence diagram updated. |
| 0.7.6 | 2026-04-02 | Task?status=requested is the only required subscription; Communication?id and CommunicationRequest?id are optional. Explains how Task.focus (introduced in 0.7.5) makes the Task subscription fire on every new message. |
| 0.7.1 | 2026-03-30 | Added the Task?id note for platforms that also want to detect read receipts (REQUESTED → COMPLETED). |
| 0.6.2 | 2026-03-27 | Communication.recipient is no longer set on replies; thread participants live on CommunicationRequest.recipient. Team query pattern changed to part-of:CommunicationRequest.recipient. |
| 0.6.1 | 2026-03-27 | Inline pseudo-code replaced by links to the example resources. Added the "Subscription behavior" section and the Pharmacy A / Clinic B walkthrough with Tasks, AuditEvents and notifications per step. |
| 0.6.0 | 2026-03-27 | Page created, split off from Individual Messaging. Introduces OZOOrganizationalCareTeam, the senderCareTeam extension pattern and the team-to-team sequence diagram. |
Team-to-team messaging enables organizations (pharmacies, clinics, hospitals) to communicate as units while maintaining individual auditability. This pattern uses the OZOOrganizationalCareTeam profile to represent organizational teams: CareTeams without a patient subject, linked to a managing Organization.
For individual messaging (RelatedPerson ↔ Practitioner), see Individual Messaging.
Both teams are recipients: CommunicationRequest.recipient lists every team participating in the thread: the addressed team (Team B) and the initiating team (Team A). The AAA proxy scopes access to threads, messages and Tasks on recipient (see AAA Proxy). Without its own entry the initiating team could not read the thread it started, and the OZO FHIR Api could not address its members. The profile enforces this with the invariant ozo-cr-sender-careteam-in-recipient.
CommunicationRequest uses the senderCareTeam extension to mark which of the recipient teams initiated the thread. This is needed because FHIR R4 CommunicationRequest.sender does not allow CareTeam references. The extension:
CommunicationRequest.requester is always an individual (who initiated the thread)CommunicationRequest.sender is always an individual (same as requester)Communication.sender is always an individual (who sent each message)subject: an organizational team has none, a patient care team (OZOCareTeam) always has one. A patient care team is never the sender's team, even when the sender is a participant: team-wide read applies to organizational teams only, so threads in a patient network keep a read state per person (see Individual Messaging). A recipient team that does not list the sender is not the sender's team either. At thread creation the sender's team is the team in extension[senderCareTeam], which must be an organizational team the requester is a participant of (the OZO FHIR Api rejects the thread otherwise). Members of the sender's team are treated as having read the message (team-wide read); members of the other recipient team(s) get an unread Task.This IG distinguishes the following roles when processing team-to-team messages:
CommunicationRequest, Communication, Task and AuditEvent.Both teams use the OZO platform. Unlike individual messaging, there is no OZO client involved: both sides are practitioners.
In practice, a single Subscription is enough for most teams:
Task?status=requested, required. Covers unread tracking and new-message notification for the team (the AAA proxy automatically scopes this to the team's ownership). Use Task? (every Task change) instead if the platform also needs to detect read receipts (REQUESTED → COMPLETED transitions).Optional additional subscriptions:
Communication?, optional. Only needed to see messages sent by your own team members. Their own Task is set to COMPLETED on send and won't match status=requested.CommunicationRequest?, optional. Only needed if you care about thread lifecycle events (creation, revoked, completed) separately from messages.Criteria form:
Subscription.criteriais a FHIR search string. A criteria without parameters is written asTask?, the resource type followed by an empty parameter list. HAPI FHIR requires the?; a bareTaskis rejected with "must be in the form {Resource Type}?[params]". Earlier versions of this page usedTask?id,Communication?idandCommunicationRequest?id.idis not a search parameter (the resource id parameter is_id); HAPI only accepted that form because it drops parameters without a value. Replace it with the empty form.
In the Netherlands, healthcare data must not be pushed in subscription notifications. All subscriptions use the notify-then-pull pattern:
This means channel.payload must be left empty. The notification only signals that something matched the subscription criteria; the subscriber is responsible for fetching the actual data.
Each subscription serves a different purpose. Understanding when notifications fire is critical for correct client implementation:
| Subscription | Purpose | Required? | Fires when |
|---|---|---|---|
Task?status=requested |
Unread tracking and new-message notification for the team. Primary mechanism. | Required | Any change to a Task that matches status=requested. This includes status transitions to REQUESTED AND content changes (like focus) on Tasks already REQUESTED. |
Communication? |
Visibility of messages sent by your own team members (sender's Task goes to COMPLETED). | Optional | A new Communication is created (POST). |
CommunicationRequest? |
Thread lifecycle changes (creation, revoked, completed). | Optional | A CommunicationRequest is created or its status changes. |
Important: When a new message arrives, the OZO FHIR Api updates the Task's
focusfield to reference the newCommunication. This ensuresTask?status=requestedfires even when the task was already in REQUESTED status: thefocuschange creates a new resource version. Thefocusfield also gives clients a direct pointer to the most recent unread message.This means
Task?status=requestedis a reliable single subscription for both unread tracking and new-message notification. The other two subscriptions are optional and only needed for specific edge cases.
A practitioner from Team A creates a new thread addressed to Team B. The process looks as follows:
CommunicationRequest object, the following fields are set:
requester is the Practitioner who initiates the conversation (for auditability)sender is the same Practitioner (individual sender)extension[senderCareTeam] is set to the CareTeam of Team A (initiating team, reply-to address). This must be an organizational CareTeam (no subject) the requester is a participant of; the OZO FHIR Api rejects the CommunicationRequest otherwisesubject is the Patient referencerecipient lists the CareTeam of Team B and the CareTeam of Team A (the same reference as in extension[senderCareTeam])status is set to ACTIVEpayload contains the initial messageTask for each member of every recipient CareTeam:
status is set to REQUESTED for the members of Team B, and to COMPLETED for the members of Team A (the sender's team: the requester sent the message and the colleagues are covered by the team-wide read)intent is set to ORDERbasedOn is set to the CommunicationRequest referencesubject is set to the Patient referenceowner is set to the individual CareTeam memberfocus is not set at thread creation (there is no initial Communication yet; the thread's initial message is on CommunicationRequest.payload)CommunicationRequest and Task by Subscription:
CommunicationRequest subscription notifies Team B of the new threadTask (status REQUESTED) tracks the unread state per team memberA practitioner from Team B responds to the thread. Replies are not addressed individually: the Communication only references the thread, and the thread's recipient list defines who participates.
Communication with the following fields:
partOf is set to the reference of the CommunicationRequestinResponseTo references the specific earlier Communication this message replies to (a quote-reply). The OZO thread model does not need it: the message belongs to the thread via partOf, and the OZO FHIR Api does not use inResponseTo for Task handling. Platforms without a reply-to-message concept leave it out.sender is set to the Practitioner from Team B (individual auditability)payload consists of text and optionally attachmentsstatus is set to COMPLETEDrecipient is not set; thread participants are defined on the CommunicationRequest.CareTeams of the CommunicationRequest and determines the sender's team: the organizational recipient team(s) (no subject) in which Communication.sender is a participant (here Team B). Then:
focus is updated to reference the new Communication (ensures the subscription fires even when status was already REQUESTED)status is set to REQUESTEDintent is set to ORDERbasedOn is set to the CommunicationRequest referencesubject is set to the Patient referenceowner is set to the individual CareTeam memberfocus is set to the new Communication referencefocus is updated to reference the new CommunicationTask?status=requested subscription fires: the Team A Tasks moved to REQUESTED and focus points to the new Communication, which creates a new version even when the status was already REQUESTED.A different practitioner from Team A follows up on the thread. This demonstrates that any team member can participate in the conversation.
Communication with the following fields:
partOf is set to the reference of the CommunicationRequestinResponseTo is optional, as abovesender is set to the different Practitioner from Team A (individual auditability; note this is a different person than the original requester)payload consists of text and optionally attachmentsstatus is set to COMPLETEDWhen a practitioner reads a message in a team thread:
AuditEvent (a read receipt) with the following properties:
type is set to http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle|access. This is the only type the OZO FHIR Api treats as a read receipt; the REST audit events the proxy creates itself use type rest and are ignored here. See Manu-Read-Messages for an example.action is set to 'R'recorded field is set to the current timestampagent.who field is set to the Practitioner who read the messageentity entries, each with one what (the profile allows entity 0..* and entity.what 0..1):
CommunicationRequest. Required: the OZO FHIR Api looks up the Task with Task?based-on=<CommunicationRequest>&owner=<agent>; without this entry the read receipt is ignored.Communication that was read. Optional but recommended: the OZO FHIR Api only completes the Task when this is the newest message in the thread, so reading an older message does not clear a newer unread one; without this entry the thread is marked read unconditionally. It also records for the NEN7510 audit trail which message was viewed.Task is queried for the Practitioner in agent.who of the AuditEvent and its status is set to COMPLETEDCareTeam(s) of the CommunicationRequest in which the reader is a participant. The Tasks of all other members of that team are set to COMPLETED as well (team-wide read). A patient care team among the recipients is left alone: its members keep their own Task.CareTeam reads the message, the message is marked as read for all the members of that CareTeam.Note: "Mark as unread" is not part of the OZO messaging model. The read state lives in the
Task(requested= unread,completed= read) and only the OZO FHIR Api changes it: on a read receipt, on a new message, and at thread creation. Clients have read-only access toTask, and there is no AuditEvent type that sets a Task back torequested. A platform that offers "mark as unread" keeps that state locally; it is not visible to other systems or, in team threads, to other team members.
The diagram below displays the team-to-team messaging flow, including thread creation and responses from both teams. {::nomarkdown}
{:/}
The following walkthrough shows a concrete example with Apotheek de Pil (Pharmacy A) and Huisarts Amsterdam (Clinic B).
A pharmacist (A.P. Otheeker) from Apotheek de Pil sends a message to Huisarts Amsterdam about a patient's medication; see Pharmacy-to-Clinic for the full CommunicationRequest. Both teams are listed as recipient: Clinic-B as addressed team and Pharmacy-A as initiating team (also referenced in extension[senderCareTeam]).
The OZO FHIR Api creates a Task for each member of both teams:
requested; see Notify-Manu-van-Weel for an example of a Task resourcecompleted (sender's team)Notifications fired:
CommunicationRequest subscription → both teams notified of the new thread (Pharmacy A is a recipient too)Task?status=requested subscription → each Clinic B practitioner notified of unread threadDr. Manu van Weel from the clinic reads the message. The OZO platform creates a read receipt AuditEvent with type iso-21089-lifecycle|access; see Manu-Read-Messages for a similar example.
The OZO FHIR Api marks the Tasks as completed. Because this is a team message, all Clinic B Tasks are completed (team-wide read):
completed (was: requested)completed (was: requested, team-wide read)completed (was: requested, team-wide read)Manu then replies with a Communication whose partOf points to the thread; see Clinic-Response-to-Pharmacy for the full resource. The OZO FHIR Api resolves the recipient teams of the thread: Manu is a participant of Clinic B, so Clinic B is the sender's team and Pharmacy A gets the unread Tasks.
Pharmacy A Tasks:
requested, focus → new Communicationrequested, focus → new CommunicationClinic B Tasks:
completed (colleague responded), focus → new CommunicationNotifications fired:
Communication subscription → Pharmacy A practitioners notified of new messageTask?status=requested subscription → each Pharmacy A practitioner notified of unread messageA.P. Otheeker has not read the reply yet (Task still REQUESTED). Pieter de Vries reads it and responds, demonstrating that any team member can participate; see Pharmacy-Followup-by-Pieter for the full Communication. Pieter is a participant of Pharmacy A, so Pharmacy A is the sender's team.
The OZO FHIR Api updates Tasks:
Pharmacy A Tasks:
completed (was: requested, team-wide read)completed (Pieter is the sender)Clinic B Tasks:
requested (was: completed)requested (was: completed)requested (was: completed)Notifications fired:
Communication subscription → Clinic B practitioners notified of new message (always fires)Task?status=requested subscription → each Clinic B practitioner notified (status changed from COMPLETED to REQUESTED AND focus updated to new Communication)Note: Even if Clinic B had not yet read the previous message (Tasks still REQUESTED), the
focusupdate would still create a new Task version and fire the subscription. Thefocusfield eliminates the no-op scenario.
Communication links to its thread via partOf, so thread searches use the part-of search parameter. based-on is a different element and is not used in the OZO messaging model.
Find messages for my team (via thread membership):
GET /Communication?part-of:CommunicationRequest.recipient=CareTeam/Pharmacy-A
The AAA proxy applies this filter automatically for client access, so a plain GET /Communication returns the same result for a team member. _include is blocked for clients by the proxy; system access can add &_include=Communication:part-of to fetch the threads in the same call.
Find all messages in a thread:
GET /Communication?part-of=CommunicationRequest/thread-id&_sort=sent
Find messages I sent:
GET /Communication?sender=Practitioner/my-id
Find all threads my team participates in:
GET /CommunicationRequest?recipient=CareTeam/Pharmacy-A
Find threads initiated by my team:
GET /CommunicationRequest?sender-careteam=CareTeam/Pharmacy-A
sender-careteam is a custom search parameter defined by this IG on extension[senderCareTeam]; see ozo-communicationrequest-sender-careteam. The HAPI FHIR server needs the OZO package installed and existing threads reindexed once; see Installing OZO Package in HAPI FHIR Server.
access)CommunicationRequest?sender-careteam= finds threads initiated by a teamFor detailed analysis of the addressing solution, see FHIR Addressing Analysis.