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:

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

  1. Open Bruno
  2. Click Open Collection
  3. 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:

EnvironmentSSE Service URLFHIR Server URLAuth
Localhttp://localhost:3001http://localhost:3000/4_0_0Paste a token from local Keycloak
Devhttps://fhir-subscription-service.dev.bwell.zonehttps://fhir-pipeline.dev.bwell.zone/4_0_0Paste a valid Okta token
Demohttps://fhir-subscription-service.dev.bwell.zonehttps://fhir-pipeline.dev.bwell.zone/4_0_0Paste a valid Okta token — used by the webhook walkthrough below

4. Set Your Token

The collection uses Bearer token auth. Before running requests:

  1. Obtain a JWT token from your identity provider (Okta for dev, Keycloak for local)
  2. In Bruno, click the environment dropdown > Configure > paste the token into the token variable

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

  1. Select the Demo environment.
  2. Click the environment dropdown → Configure and set:
    • token — your Okta bearer token
    • webhook_url — your webhook.site (or other) receiver URL
    • demo_tenant — leave as cdex-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

SymptomLikely cause
No notification arrivesYou didn't wait ~30s after creating the subscription before creating the Task.
Notifications keep arriving for Tasks you didn't createAnother active subscription (yours or someone else's) shares your tenant and matches broadly — clean up unused subscriptions.
401 on any requestToken expired (Okta tokens last ~1 hour) — refresh and paste a new one.
400 creating the SubscriptionCheck 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-category security tag. The CDC connector should filter this out, so no SSE notification is expected. Use this to verify the filtering pipeline.

Variables

VariableDescriptionSet By
sse_urlSSE service base URLEnvironment
fhir_urlFHIR server base URLEnvironment
tokenJWT Bearer tokenYou (manually)
ownerOwner/access code for meta.security tagsEnvironment (default: bwell)
subscription_idFHIR Subscription IDAuto-set by create-subscription scripts
webhook_urlYour webhook receiver URL (webhook.site or your own endpoint)You (manually, in the Demo environment)
demo_tenantAccess/owner code isolating your demo-clean/ resources from others in a shared environmentEnvironment (default: cdex-demo)
task_idFHIR Task IDAuto-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 status is active and the criteria matches the resource type you created.
  • Local setup: Run docker compose up in the repo root to start Kafka, ClickHouse, Redis, and MongoDB. The FHIR server (localhost:3000) must be running separately.

Did this page help you?