A critical potassium level of 6.8 mEq/L comes back from the lab. The result sits in the EHR database. The downstream care coordination app polls for updates every 15 minutes. For the next 14 minutes, nobody outside the lab knows this patient is at risk for cardiac arrest.
This is not a hypothetical scenario. It is the default architecture of most clinical integration systems in production today. According to research published in the Journal of the American Medical Informatics Association, 48.7% of EHR-related safety events involve laboratory orders and results, with delayed notification a recurring contributor. A 2022 study in JAMIA found that nearly 30% of primary care physicians reported missing test results that delayed patient care.
The problem is not the EHR. The problem is the integration pattern. Polling-based architectures introduce latency that is fundamentally incompatible with time-sensitive clinical workflows. ADT discharge events trigger care transitions 15-30 minutes late. Medication reconciliation apps miss orders placed between polling intervals. Quality reporting dashboards show data that is always stale.
FHIR R5 introduced a complete redesign of the Subscription framework that replaces this polling model with standardized, topic-based, real-time notifications. For healthcare architects building systems where minutes matter, this is the most consequential change in the FHIR specification since the introduction of SMART on FHIR.
Why Traditional Webhooks Fail in Healthcare
Before diving into the FHIR solution, it is worth understanding why generic webhook implementations have not solved this problem already. Webhooks are well-established in SaaS integrations. Stripe, GitHub, and Twilio all use them effectively. But healthcare is different in five specific ways that make generic webhooks inadequate.
No standard payload format. When Epic sends a webhook, the payload structure differs from Cerner's, which differs from MEDITECH's. Every integration requires custom parsing logic. A consumer application that receives notifications from three different EHRs needs three different deserialization paths. This is the opposite of interoperability.
No retry semantics. If a webhook delivery fails, what happens? In most healthcare webhook implementations, the answer is "nothing" or "it depends." There is no standard for exponential backoff, dead letter queues, or maximum retry counts. Failed notifications are silently lost, and the consuming application has no way to know what it missed.
No topic filtering. Generic webhooks are typically all-or-nothing. An application subscribes to "patient events" and receives every admission, discharge, transfer, lab result, order, note, and demographic update. The consumer must filter locally, wasting bandwidth and processing time on events it does not care about.
No authentication standard. How does the consumer verify that a webhook actually came from the EHR and was not spoofed? HMAC signatures? Mutual TLS? IP allowlists? Every vendor implements this differently, and many do not implement it at all. In an industry governed by HIPAA, this is a compliance gap.
No guaranteed delivery. Webhooks are fire-and-forget by default. The sender has no mechanism to track whether the consumer received, processed, or acknowledged the notification. There is no heartbeat to distinguish "no events occurred" from "the consumer is down and notifications are being dropped."
These are not edge cases. They are fundamental architectural requirements for clinical systems where a missed notification can delay treatment. FHIR R5 Subscriptions address every one of them.
FHIR Subscriptions (R5): The Topic-Based Model
The FHIR R5 Subscription framework replaces the criteria-based model from R4 with a topic-based publish/subscribe architecture. The design separates three concerns that were previously conflated: what triggers a notification, who receives it and how, and what the delivery status is.
SubscriptionTopic: What Triggers a Notification
A SubscriptionTopic is a server-defined resource that describes a specific event or data change that can trigger notifications. Topics are defined by the FHIR server administrator and published for clients to discover. They specify:
- Resource triggers: Which resource type and what operation (create, update, delete) fires the event
- Filter parameters: What criteria subscribers can use to narrow the notifications they receive
- Canonical URL: A stable identifier that clients use to reference the topic
- Notification shape: What resources are included in the notification bundle
For example, a topic for critical lab results might be defined as:
{
"resourceType": "SubscriptionTopic",
"url": "http://example.org/SubscriptionTopic/new-lab-result",
"status": "active",
"resourceTrigger": [{
"resource": "Observation",
"supportedInteraction": ["create"],
"fhirPathCriteria": "category.coding.where(code='laboratory').exists()"
}],
"canFilterBy": [{
"resource": "Observation",
"filterParameter": "patient",
"modifier": ["eq"]
}, {
"resource": "Observation",
"filterParameter": "code",
"modifier": ["eq", "in"]
}]
} This tells subscribers: "I will fire when a new laboratory Observation is created, and you can filter by patient and/or observation code."
Subscription: Who Subscribes and How
A Subscription resource is created by a client application to request notifications for a specific topic, delivered via a specific channel. The subscription specifies:
- Topic reference: Which SubscriptionTopic to subscribe to
- Channel type: How to deliver notifications (rest-hook, websocket, email, messaging)
- Endpoint: Where to send notifications
- Filter criteria: Narrowing which events within the topic are relevant
- Payload content: Whether to include the full resource, just the ID, or an empty notification
{
"resourceType": "Subscription",
"status": "requested",
"topic": "http://example.org/SubscriptionTopic/new-lab-result",
"channelType": {
"system": "http://terminology.hl7.org/CodeSystem/subscription-channel-type",
"code": "rest-hook"
},
"endpoint": "https://care-app.example.com/fhir/notifications",
"filterBy": [{
"filterParameter": "patient",
"value": "Patient/patient-123"
}],
"content": "full-resource",
"heartbeatPeriod": 120
} SubscriptionStatus: Delivery Tracking
Every notification bundle includes a SubscriptionStatus resource that provides delivery metadata: the notification type (handshake, heartbeat, event-notification, query-status), the count of events since the subscription was created, and error information if applicable. This solves the "silent failure" problem that plagues generic webhooks. If a consumer stops receiving heartbeats, it knows notifications are being dropped and can take corrective action.
Channel Types: Choosing the Right Delivery Mechanism
FHIR R5 defines four channel types, each optimized for different integration patterns. Choosing the right channel is an architectural decision that depends on latency requirements, consumer capabilities, and deployment topology.
rest-hook (HTTP POST)
The most common channel type. The FHIR server sends an HTTP POST to the subscriber's endpoint with the notification bundle as the request body. Best for server-to-server integrations where the consumer has a publicly accessible HTTPS endpoint. Latency is sub-second for most implementations. This is the channel you should default to unless you have a specific reason to use something else.
websocket
The FHIR server maintains a persistent WebSocket connection with the client. Ideal for real-time clinical dashboards, monitoring applications, and any scenario where the consumer is a browser-based application that cannot receive inbound HTTP requests. Slightly lower latency than rest-hook since there is no connection establishment overhead per notification, but requires persistent connection management.
Notifications sent via SMTP. Appropriate for non-urgent alerts where the recipient is a human rather than a system. Use cases include daily quality measure summaries, non-critical lab result notifications to referring physicians, and administrative alerts. Not suitable for time-sensitive clinical workflows due to inherent email delivery latency.
messaging
Notifications sent as FHIR Messaging bundles. Best for complex workflows that require message routing, intermediary processing, or integration with existing FHIR Messaging infrastructure. Less common than rest-hook but important for organizations that have invested in FHIR Messaging for their integration backbone.
The Subscription Lifecycle
Understanding the subscription lifecycle is critical for building reliable notification consumers. The lifecycle follows a well-defined state machine:
- Client creates Subscription (POST /Subscription) with status
requested - Server validates the subscription against the referenced SubscriptionTopic and returns
201 Created - Server sends handshake notification to the specified endpoint to verify reachability
- Client responds 200 to the handshake, confirming the endpoint is operational
- Server activates the subscription, updating status to
active - Events trigger notifications — when matching data changes occur, the server sends notification bundles
- Client processes and acknowledges each notification with an HTTP 200 response
- Heartbeats maintain liveness — periodic empty notifications confirm the channel is operational
If the server receives errors from the endpoint, it transitions the subscription to error status after a configurable number of retries. This explicit state tracking is a significant improvement over generic webhooks where failures are invisible.
Architecture: Event-Driven Clinical Systems
In a production architecture, the FHIR server acts as a subscription engine that sits between event sources and consumer applications. This hub-and-spoke model decouples producers from consumers and centralizes notification logic.
Event sources write data to the FHIR server through standard FHIR APIs. A lab system creates Observation resources. An ADT system creates and updates Encounter resources. A pharmacy system creates MedicationRequest resources. These systems do not need to know about subscriptions at all — they simply perform CRUD operations on FHIR resources.
The subscription engine evaluates each write against active SubscriptionTopics. When a match is found, it assembles the notification bundle, applies subscriber-specific filters, and delivers the notification via the subscriber's chosen channel.
Consumer applications subscribe to the topics they care about and receive filtered, real-time notifications. A care transition app subscribes to ADT discharge events. A clinical dashboard subscribes to critical lab results. A quality reporting system subscribes to all encounters for measure calculation. Each consumer only receives the events relevant to its function.
This architecture enables clinical workflows that were previously impractical with polling. A critical lab result can trigger a care team notification within seconds of the result being filed. A discharge event can initiate care transition workflows before the patient leaves the building. A new medication order can be routed to a pharmacist for review in real time.
Practical Implementation: HAPI FHIR Server
The HAPI FHIR server, the most widely used open-source FHIR server, supports topic-based subscriptions. Here is a practical walkthrough of setting up a subscription that notifies a care coordination app when critical lab results are filed.
Step 1: Enable subscriptions in HAPI configuration
# application.yaml
hapi:
fhir:
subscription:
resthook_enabled: true
websocket_enabled: true Step 2: Create a SubscriptionTopic
POST /fhir/SubscriptionTopic
Content-Type: application/fhir+json
{
"resourceType": "SubscriptionTopic",
"url": "http://your-org.com/SubscriptionTopic/critical-lab",
"status": "active",
"title": "Critical Laboratory Results",
"resourceTrigger": [{
"resource": "Observation",
"supportedInteraction": ["create", "update"],
"fhirPathCriteria": "category.coding.where(code='laboratory').exists() and interpretation.coding.where(code='critical').exists()"
}],
"canFilterBy": [{
"resource": "Observation",
"filterParameter": "patient"
}],
"notificationShape": [{
"resource": "Observation",
"include": ["Observation:patient", "Observation:performer"]
}]
} Step 3: Subscribe from your consumer application
POST /fhir/Subscription
Content-Type: application/fhir+json
{
"resourceType": "Subscription",
"status": "requested",
"topic": "http://your-org.com/SubscriptionTopic/critical-lab",
"reason": "Care coordination critical lab alerts",
"channelType": {
"system": "http://terminology.hl7.org/CodeSystem/subscription-channel-type",
"code": "rest-hook"
},
"endpoint": "https://care-app.example.com/fhir/notifications",
"heartbeatPeriod": 60,
"content": "full-resource",
"maxCount": 10
} Step 4: Handle incoming notifications
# Python notification handler (Flask example)
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
@app.route('/fhir/notifications', methods=['POST'])
def handle_notification():
bundle = request.get_json()
# Extract SubscriptionStatus (first entry)
status_entry = bundle['entry'][0]['resource']
notification_type = status_entry['type']
if notification_type == 'handshake':
# Server is verifying our endpoint
return jsonify({"status": "ok"}), 200
if notification_type == 'heartbeat':
# Channel liveness check — log and acknowledge
return jsonify({"status": "ok"}), 200
if notification_type == 'event-notification':
# Process clinical data
for entry in bundle['entry'][1:]:
resource = entry.get('resource', {})
if resource.get('resourceType') == 'Observation':
process_critical_lab(resource)
return jsonify({"status": "processed"}), 200
return jsonify({"status": "unknown type"}), 400
def process_critical_lab(observation):
patient_ref = observation.get('subject', {}).get('reference', '')
code = observation.get('code', {}).get('text', 'Unknown')
value = observation.get('valueQuantity', {})
# Idempotency: check if we have already processed this observation
obs_id = observation.get('id')
if already_processed(obs_id):
return
# Trigger clinical workflow
alert_care_team(patient_ref, code, value)
mark_processed(obs_id) Idempotency is critical. FHIR servers may retry failed deliveries, and network issues can cause duplicate notifications. Your handler must be idempotent — processing the same notification twice should have no adverse effect. Use the Observation ID (or a combination of subscription event number and resource ID) as an idempotency key.
Backport to R4: Using the R5 Model Today
Many organizations are not yet on FHIR R5. The Subscriptions R5 Backport Implementation Guide defines a standard way to use the topic-based subscription model on R4 and R4B servers. This is not a theoretical exercise — Smile CDR, HAPI FHIR, and other servers support the backport.
The backport works by defining extensions on the R4 Subscription resource that carry the R5 fields. Key differences from native R5:
- SubscriptionTopic is represented as a Basic resource with a specific profile, since the SubscriptionTopic resource type does not exist in R4
- Subscription.criteria is still required in R4 but can reference the topic canonical URL via extension
- Notification bundles use the same structure as R5 but with backport-specific profiles
- Channel types are identical to R5
If your organization is on R4 and planning a migration to R5, implementing the backport now means your subscription consumers will require minimal changes when you upgrade. The notification bundle format and channel semantics are identical.
CDS Hooks: The Complementary Pattern
FHIR Subscriptions and CDS Hooks are often discussed together, but they solve fundamentally different problems. Understanding when to use each — and when to use both — is essential for healthcare architects designing clinical decision support systems. (For a deeper look at clinical decision support architecture, see our guide on building AI-driven clinical decision support systems.)
FHIR Subscriptions are asynchronous and event-driven. They fire when data changes, regardless of whether a user is actively working in the EHR. No user interaction is required. The notification flows from server to consumer in the background.
CDS Hooks are synchronous and action-triggered. They fire when a clinician performs a specific action in the EHR — opening a patient chart (patient-view), signing an order (order-sign), or selecting a medication (order-select). The CDS service returns decision support cards that appear inline in the clinician's workflow.
| Dimension | FHIR Subscriptions | CDS Hooks |
|---|---|---|
| Timing | Asynchronous, event-driven | Synchronous, user-action triggered |
| Trigger | Data change on server | Clinician action in EHR |
| User interaction | Not required | Required (returns inline cards) |
| Latency requirement | Sub-second to minutes | Sub-second (blocks user workflow) |
| Use cases | Lab alerts, discharge notifications, medication changes | Drug interaction warnings, order recommendations, documentation reminders |
| Direction | Server pushes to consumer | EHR calls external service |
In a well-designed clinical system, these patterns are complementary. A FHIR Subscription notifies the care team app when a critical lab result is filed (async, no user in the loop). A CDS Hook fires when the physician opens the patient chart and displays the critical result with recommended actions (sync, at point of care). Both patterns reference the same FHIR data. Neither replaces the other.
Real-World Topic Examples
To make the topic model concrete, here are SubscriptionTopic patterns that address common clinical integration challenges. These map directly to workflows that healthcare architects encounter in production:
- Critical lab result for specific patient: Trigger on Observation create where
category=laboratoryandinterpretation=critical, filterable bypatient. Enables per-patient critical result alerting for care coordination apps. - ADT discharge event: Trigger on Encounter update where
statustransitions tofinishedandclass=inpatient. Powers care transition workflows, readmission risk assessments, and post-discharge follow-up scheduling. - New medication order requiring pharmacist review: Trigger on MedicationRequest create where
status=activeandintent=order. Routes new orders to pharmacy review queues in real time instead of on the next polling cycle. - Patient demographic update: Trigger on Patient update. Keeps downstream systems (billing, care management, patient portals) synchronized with the source of truth without polling.
- Appointment no-show: Trigger on Appointment update where
statustransitions tonoshow. Enables automated outreach, rescheduling workflows, and care gap identification.
Each of these topics can support multiple subscribers with different channel types and filters. The ADT discharge topic might have a rest-hook subscriber for the care transition app, a websocket subscriber for the real-time bed management dashboard, and an email subscriber for the referring physician notification system.
Migration Checklist: From Polling to Subscriptions
For healthcare architects planning to migrate from polling-based integrations to FHIR Subscriptions, here is a practical checklist:
- Audit current polling integrations. Document every system that polls your FHIR server, what resources they query, how frequently, and what they do with the results. This inventory becomes your SubscriptionTopic requirements.
- Evaluate server support. Confirm your FHIR server supports topic-based subscriptions (R5 native or R4 backport). HAPI FHIR, Smile CDR, and several commercial servers support this today.
- Define SubscriptionTopics. Map each polling pattern to a SubscriptionTopic. A system that polls for new lab results every 5 minutes becomes a subscriber to a "new laboratory Observation" topic.
- Build idempotent consumers. Design your notification handlers to be idempotent from day one. Duplicate notifications will happen.
- Implement heartbeat monitoring. Set heartbeat periods on all subscriptions and alert when heartbeats stop. This replaces the implicit liveness check that polling provides (if polling returns results, the server is up).
- Run in parallel. Keep polling running alongside subscriptions during the migration period. Compare results to verify that subscriptions are delivering all expected notifications before decommissioning the polling integration.
- Monitor subscription status. Build operational dashboards that track subscription status (active, error, off) and notification delivery metrics.
Building What Comes Next
The shift from polling to event-driven architecture in healthcare is not optional — it is inevitable. As clinical workflows demand real-time data for patient safety, as interoperability standards mature, and as regulatory requirements like TEFCA push for standardized data exchange, FHIR Subscriptions provide the foundation for the next generation of clinical integration.
The R5 Subscription framework is not perfect — topic discovery, cross-server subscriptions, and subscription management at scale are still evolving. But the core model of topic-based, channel-agnostic, status-tracked notifications is a genuine architectural improvement over both polling and ad-hoc webhooks.
At Nirmitee, we build healthcare systems with FHIR-native architectures designed for real-time clinical workflows. If your team is evaluating event-driven patterns for clinical integration, we would welcome the conversation.
Looking to build a robust healthcare platform? Our Healthcare Software Product Development team turns complex requirements into production-ready systems. We also offer specialized Healthcare Interoperability Solutions services. Talk to our team to get started.
Frequently Asked QuestionsWhat is the difference between FHIR R4 and R5 Subscriptions?
R4 uses a criteria-based model where subscribers specify a search query string. R5 replaces this with a topic-based model where the server defines SubscriptionTopics and subscribers reference those topics. The R5 model adds explicit status tracking, heartbeat support, and standardized notification bundles. The R5 Backport IG allows R4 servers to use the topic-based model via extensions.
Can I use FHIR Subscriptions with Epic, Cerner, or MEDITECH?
Support varies by vendor and version. Epic supports event-based notifications through its own mechanism. Cerner (now Oracle Health) has subscription capabilities. Check your vendor's FHIR implementation guide for current subscription support. For vendors that do not yet support R5 Subscriptions natively, an intermediary FHIR server (like HAPI) can bridge the gap.
How do FHIR Subscriptions handle PHI and HIPAA compliance?
FHIR Subscriptions inherit the security model of the FHIR server. Notifications should be sent over TLS, endpoint URLs should use HTTPS, and subscription creation should require appropriate authorization. The content field allows you to choose between full-resource (includes PHI in notification), id-only (notification contains only the resource ID; consumer fetches the full resource separately), or empty (notification that something changed, no data). For maximum security, use id-only so PHI is never transmitted in notification payloads.
What happens when a subscription consumer goes down?
The FHIR server tracks delivery failures and transitions the subscription to error status after a configurable number of retries. Heartbeat notifications provide an additional liveness check. When the consumer recovers, it can query for missed events using the $events operation or the event count in SubscriptionStatus to identify gaps. This is a significant improvement over generic webhooks where failures are invisible.
