FHIR Resource Subscriptions
Follow this guide to receive real-time notifications on FHIR resource updates.
Bruno is a fast, git-friendly API client. This collection lets you test the FHIR Subscription Service endpoints directly from the Bruno desktop app.
The service delivers FHIR resource-change notifications over two channels:
- Webhook (
rest-hook) — wePOSTa lightweight notification to a URL you provide whenever a matching resource changes. See Webhook (rest-hook) Walkthrough below. - SSE (Server-Sent Events) — you hold an open HTTP connection and receive events as they happen, with replay on reconnect. See End-to-End Testing Walkthrough (SSE) below.
Both channels are built on FHIR R4, using the Subscriptions R5 Backport IG — the standard way to bring topic-based subscriptions to an R4 server. If you're integrating a webhook receiver (for example, as part of a Da Vinci CDex task-based data exchange), start with the webhook section.
Getting Started
1. Install Bruno
Download from usebruno.com (macOS, Windows, Linux).
2. Open the Collection
- Open Bruno
- Click Open Collection
- Navigate to this repository and select the
bruno/folder
Bruno reads the .bru files directly from disk — any changes you make are saved as file edits, which means they show up in git diff.
3. Select an Environment
Use the environment dropdown (top-right) to switch between:
| Environment | SSE Service URL | FHIR Server URL | Auth |
|---|---|---|---|
| Local | http://localhost:3001 | http://localhost:3000/4_0_0 | Paste a token from local Keycloak |
| Dev | https://fhir-subscription-service.dev.bwell.zone | https://fhir-pipeline.dev.bwell.zone/4_0_0 | Paste a valid Okta token |
| Demo | https://fhir-subscription-service.dev.bwell.zone | https://fhir-pipeline.dev.bwell.zone/4_0_0 | Paste a valid Okta token — used by the webhook walkthrough below |
4. Set Your Token
The collection uses Bearer token auth. Before running requests:
- Obtain a JWT token from your identity provider (Okta for dev, Keycloak for local)
- In Bruno, click the environment dropdown > Configure > paste the token into the
tokenvariable
Collection Structure
bruno/
├── bruno.json # Collection manifest
├── collection.bru # Collection-level auth (Bearer token)
├── environments/
│ ├── Local.bru # Local dev environment
│ ├── Dev.bru # Dev environment
│ └── Demo.bru # Demo environment (used by demo-clean/)
├── demo-clean/ # Webhook (rest-hook) walkthrough — isolated tenant
│ ├── A1-create-subscription.bru # POST /Subscription (channel.type=rest-hook)
│ ├── A2-create-task-direct.bru # POST /Task (direct to FHIR — triggers the wake-up)
│ ├── A3-complete-task.bru # PUT /Task/{id} (status=completed + Task.output)
│ ├── A4-cleanup.bru # DELETE /Subscription/{id}
│ └── A5-cleanup-task.bru # DELETE /Task/{id}
├── health/ # Health & readiness checks
│ ├── sse-health.bru # GET /actuator/health
│ ├── sse-liveness.bru # GET /actuator/health/liveness
│ ├── sse-readiness.bru # GET /actuator/health/readiness
│ └── fhir-metadata.bru # GET /metadata (FHIR server)
├── subscription/ # FHIR Subscription management
│ ├── create-observation-subscription.bru # POST /Subscription (Observation)
│ ├── create-condition-subscription.bru # POST /Subscription (Condition)
│ ├── create-encounter-subscription.bru # POST /Subscription (Encounter)
│ ├── get-subscription.bru # GET /Subscription/{id}
│ └── list-active-subscriptions.bru # GET /Subscription?status=active
├── observation/ # Create Observations (triggers SSE events)
│ ├── create-observation-heart-rate.bru # POST /Observation (heart rate)
│ ├── create-observation-blood-pressure.bru# POST /Observation (blood pressure)
│ └── create-observation-no-sensitivity.bru# POST /Observation (negative test)
├── other-resources/ # Create other FHIR resources
│ ├── create-condition.bru # POST /Condition
│ ├── create-encounter.bru # POST /Encounter
│ └── create-medication-request.bru # POST /MedicationRequest
├── sse-connect/ # Connect to SSE stream
│ ├── connect-sse-client.bru # GET /sse/Subscription/{id}/$events
│ └── connect-sse-with-replay.bru # GET /sse/Subscription/{id}/$events (with Last-Event-Id)
└── cleanup/
└── deactivate-subscription.bru # PUT /Subscription/{id} (set status=off)
Webhook (rest-hook) Walkthrough
This walks through the webhook delivery channel using the demo-clean/ folder and the Demo environment. It creates a Subscription and a Task under an isolated tenant so the notifications you see are only your own — useful in a shared environment where many other resources are changing at the same time.
The model: you register a Subscription telling the service where to send notifications. When a matching Task is created or updated, the service POSTs a small notification — just a reference to the Task that changed, no clinical data — to your endpoint. You then fetch the Task to see its current state and any result location.
Prerequisites
- A webhook receiver URL to send notifications to. For testing, create a free unique URL at webhook.site and keep the page open in a browser tab — you'll see notifications arrive there live.
- A valid Okta bearer token.
Setup
- Select the Demo environment.
- Click the environment dropdown → Configure and set:
token— your Okta bearer tokenwebhook_url— your webhook.site (or other) receiver URLdemo_tenant— leave ascdex-demo, or use your own value; it just needs to be consistent across the requests below
Step 1 — Create the Subscription
Run demo-clean > A1-create-subscription. This creates a FHIR Subscription with channel.type: rest-hook pointing at your webhook_url, scoped to the demo_tenant. The response id is saved to subscription_id.
Wait ~30 seconds before continuing — the service refreshes its list of active subscriptions every 30 seconds.
Step 2 — Create a Task
Run demo-clean > A2-create-task-direct. This creates a Task directly in the FHIR server, tagged with the same demo_tenant. The response id is saved to task_id.
Within a few seconds, a notification should arrive at your webhook_url. It's a FHIR Bundle carrying a SubscriptionStatus (encoded as a Parameters resource, per the Backport IG), shaped like this:
{
"resourceType": "Bundle",
"id": "<uuid>",
"type": "history",
"timestamp": "<ISO-8601 timestamp>",
"entry": [{
"fullUrl": "urn:uuid:<uuid>",
"resource": {
"resourceType": "Parameters",
"id": "<uuid>",
"meta": {
"profile": ["http://hl7.org/fhir/uv/subscriptions-backport/StructureDefinition/backport-subscription-status-r4"]
},
"parameter": [
{ "name": "subscription", "valueReference": { "reference": "Subscription/<subscription_id>" } },
{ "name": "topic", "valueCanonical": "https://bwell.zone/fhir/SubscriptionTopic/resource-change" },
{ "name": "status", "valueCode": "active" },
{ "name": "type", "valueCode": "event-notification" },
{ "name": "events-since-subscription-start", "valueString": "1" },
{ "name": "notification-event", "part": [
{ "name": "event-number", "valueString": "1" },
{ "name": "timestamp", "valueInstant": "<ISO-8601 timestamp>" },
{ "name": "focus", "valueReference": { "reference": "Task/<task_id>" } }
]}
]
},
"request": { "method": "GET", "url": "Subscription/<subscription_id>/$status" },
"response": { "status": "200" }
}]
}Most of this is subscription-status protocol metadata — status: active describes the subscription's status, not the Task's. The only Task-specific content is the focus reference: no Task.status, and no clinical data. That's the point — it's a notification to go look, not the data itself. Fetch the Task (via the API you use to submit tasks) to see its actual status and, once complete, its result.
Step 3 — Complete the Task
Run demo-clean > A3-complete-task. This updates the Task to status: completed and adds a result location to Task.output. A second notification arrives at your webhook_url, with the same shape as before, referencing the same Task.
At this point your receiver would fetch the Task (via the API you use to submit tasks) and read Task.output to get the result location.
Step 4 — Clean up
Run demo-clean > A4-cleanup to delete the Subscription, then demo-clean > A5-cleanup-task to delete the test Task. Deleting the subscription before creating another one avoids leaving stale subscriptions active.
Webhook Troubleshooting
| Symptom | Likely cause |
|---|---|
| No notification arrives | You didn't wait ~30s after creating the subscription before creating the Task. |
Notifications keep arriving for Tasks you didn't create | Another active subscription (yours or someone else's) shares your tenant and matches broadly — clean up unused subscriptions. |
401 on any request | Token expired (Okta tokens last ~1 hour) — refresh and paste a new one. |
400 creating the Subscription | Check that meta.security and meta.source are present — the FHIR server rejects writes without them. |
End-to-End Testing Walkthrough (SSE)
Follow these steps in order to verify the full SSE flow:
Step 1 — Verify the service is running
Run health > sse-health. You should get {"status":"UP"} with component statuses for Redis, ClickHouse, and Kafka.
Step 2 — Create a Subscription
Run subscription > create-observation-subscription. This creates a FHIR Subscription that watches for Observation changes. The response id is automatically saved to the subscription_id environment variable.
Step 3 — Connect to SSE
Note: Bruno has limited SSE streaming support. For live streaming, use curl:
curl -N "http://localhost:3001/sse/Subscription/<subscription_id>/\$events" \ -H "Authorization: Bearer <your_token>" \ -H "Accept: text/event-stream"
You should receive a handshake event followed by periodic heartbeat events.
Step 4 — Trigger an event
In a separate Bruno window/tab, run observation > create-observation-heart-rate. This POSTs an Observation to the FHIR server, which triggers a MongoDB CDC event through Kafka to the SSE service.
Watch your curl terminal — you should see a notification event with a FHIR Bundle payload.
Step 5 — Test replay
Disconnect and reconnect with the Last-Event-Id header:
curl -N "http://localhost:3001/sse/Subscription/<subscription_id>/\$events" \
-H "Authorization: Bearer <your_token>" \
-H "Accept: text/event-stream" \
-H "Last-Event-Id: 0"Missed events are replayed from ClickHouse before the handshake.
Step 6 — Clean up
Run cleanup > deactivate-subscription to set the subscription status to off.
Negative Tests
- create-observation-no-sensitivity — Creates an Observation without the
sensitivity-categorysecurity tag. The CDC connector should filter this out, so no SSE notification is expected. Use this to verify the filtering pipeline.
Variables
| Variable | Description | Set By |
|---|---|---|
sse_url | SSE service base URL | Environment |
fhir_url | FHIR server base URL | Environment |
token | JWT Bearer token | You (manually) |
owner | Owner/access code for meta.security tags | Environment (default: bwell) |
subscription_id | FHIR Subscription ID | Auto-set by create-subscription scripts |
webhook_url | Your webhook receiver URL (webhook.site or your own endpoint) | You (manually, in the Demo environment) |
demo_tenant | Access/owner code isolating your demo-clean/ resources from others in a shared environment | Environment (default: cdex-demo) |
task_id | FHIR Task ID | Auto-set by A2-create-task-direct |
Tips
- Token expired? Okta tokens last ~1 hour. Refresh and paste a new one into the environment config.
- No SSE events? Check that the subscription
statusisactiveand thecriteriamatches the resource type you created. - Local setup: Run
docker compose upin the repo root to start Kafka, ClickHouse, Redis, and MongoDB. The FHIR server (localhost:3000) must be running separately.
Updated 3 days ago
