HomeEHR Interoperability & Data ExchangeHL7 FHIR API Health Data Exchange Simulator

🔗 HL7 FHIR API Health Data Exchange Simulator

A simulation tool for exchanging health data using the HL7 FHIR API standard.

EHR Interoperability & Data Exchange2DModerate60 FPS
hl7-fhir-api-data-exchange-simulator ↗ Open standalone

SMART App Discovery & the FHIR Capability Statement

Before a single clinical resource is ever requested, an interoperable app must learn what a FHIR server actually supports. The CapabilityStatement resource and the SMART on FHIR well-known configuration document form the discovery contract that lets any conformant client — an EHR-embedded SMART app, a patient-facing app, or a population-health pipeline — introspect a server's capabilities before authenticating or querying.

  • R4: FHIR version simulated (HL7 FHIR Release 4 (4.0.1))
  • /.well-known/smart-configuration: Discovery endpoint (SMART App Launch spec)
  • US Core: Core profile set (ONC-mandated USCDI-aligned profiles)
  • 40–90ms: Typical discovery latency (cached CapabilityStatement)

What the CapabilityStatement actually declares

Every conformant FHIR server exposes a CapabilityStatement at GET [base]/metadata. This resource is itself a FHIR resource — machine-readable, versioned, and queryable — that enumerates:

• FHIR version supported (this simulation targets R4, 4.0.1) and the server's implementation guides — typically US Core 6.1.0, which operationalizes the ONC-mandated USCDI (United States Core Data for Interoperability) data classes as FHIR profiles • REST interaction modes per resource type: read, vread, search-type, create, update, patch, delete, history — a server may support Patient.search but not Patient.delete • Supported search parameters per resource, e.g. Observation?patient=&category=&code=&date= — critical because search parameter support varies wildly between vendor EHR FHIR implementations • Supported _include / _revinclude chains for fetching related resources in one round trip • Security section pointing to the OAuth2 authorization and token endpoints via the SMART extension

A client that skips discovery and hard-codes assumptions about server capabilities is the single most common cause of production interoperability failures — a query that works against a sandbox instance silently breaks against a live Hospital B deployment running an older CapabilityStatement.

ONC's 2015 Edition Cures Update Certification (45 CFR 170.315(g)(10)) requires certified health IT to publish both the CapabilityStatement and the SMART well-known configuration document without requiring authentication — discovery must be an open, unauthenticated first step.

SMART on FHIR launch discovery

Alongside the CapabilityStatement, SMART on FHIR (Substitutable Medical Applications, Reusable Technologies) defines a parallel discovery document at /.well-known/smart-configuration. This JSON document tells the app:

• authorization_endpoint and token_endpoint — where to send the OAuth2 dance • capabilities array — which SMART capabilities the server implements: launch-ehr, launch-standalone, client-public, client-confidential-symmetric, sso-openid-connect, context-passing-patient • scopes_supported — the exact SMART scope grammar the authorization server will honor, e.g. patient/Observation.rs or system/*.read for backend services • code_challenge_methods_supported — confirms PKCE (Proof Key for Code Exchange) support, mandatory for public clients since SMART App Launch 2.0

Hospital A's app inspects this document to decide whether it can even attempt an EHR-launched flow (context handed off from inside the EHR session) versus a standalone launch (the app initiates its own auth flow and asks the user to select a patient).

Why discovery matters for cross-vendor interoperability

Two hospitals running different EHR vendor platforms may both claim "FHIR R4 support" while implementing substantially different subsets of US Core. Discovery is the mechanism that prevents integration teams from guessing:

• A vendor may support Patient.search by _id and identifier but not by name+birthdate — a combination other vendors treat as mandatory • Bulk Data support ($export) is an optional CapabilityStatement extension (http://hl7.org/fhir/uv/bulkdata) — many production EHR instances only enable it for specific client registrations • Terminology bindings differ: one system may bind Observation.code strictly to LOINC, while a legacy bridge still emits local codes requiring a ConceptMap crosswalk

TEFCA (Trusted Exchange Framework and Common Agreement), CommonWell Health Alliance, and Carequality all layer additional conformance requirements on top of base FHIR discovery — a Qualified Health Information Network (QHIN) participant must additionally publish endpoints in a shared directory so trading partners can resolve capabilities without a manual integration contract for every new connection.

OAuth2 Token Exchange & SMART Scope Negotiation

FHIR itself says nothing about authentication — it is a data model and REST API convention. Authorization is delegated entirely to OAuth2, profiled by the SMART App Launch specification. This stage simulates the authorization code grant: redirect, consent, code exchange, and the minting of a scoped, time-limited access token the API gateway will validate on every subsequent call.

  • Authorization Code: Grant type (+ PKCE for public clients)
  • 5–60 min: Typical token TTL (access_token expiry)
  • Resource + interaction: Scope granularity (e.g. patient/Observation.rs)
  • offline_access: Refresh capability (optional refresh_token scope)

The authorization code dance, step by step

1. The app redirects the browser (or embedded webview) to Hospital B's /authorize endpoint with client_id, redirect_uri, a requested scope string, a PKCE code_challenge, and a state parameter for CSRF protection.

2. The authorization server authenticates the clinician or patient (often via the hospital's existing identity provider — SAML or OIDC federation) and presents a consent screen listing exactly which scopes are being requested: read-only access to the patient's Observations, or read/write access to MedicationRequest, for example.

3. On approval, the server redirects back to redirect_uri with a short-lived, single-use authorization code and the original state value.

4. The app's backend (confidential client) or the app itself (public client, using PKCE code_verifier instead of a client secret) POSTs to /token, exchanging the code for an access_token, an optional refresh_token, the granted scope, and launch context parameters like patient=<id> and encounter=<id>.

5. Every subsequent FHIR API call includes Authorization: Bearer <access_token>. The API gateway validates the token's signature, expiry, and scope on every request before it ever reaches the FHIR resource server.

SMART App Launch 2.0 makes PKCE mandatory for all public clients (apps that cannot securely hold a client secret, such as mobile or SPA apps) — closing an authorization-code-interception vulnerability class that was common in early mobile SMART implementations.

SMART scope grammar and least-privilege access

SMART scopes encode three dimensions in a single string: context (patient/, user/, or system/), resource type (Patient, Observation, *), and permitted interaction (.read, .write, .rs for read+search, .cruds for full CRUD). Examples simulated in this gateway:

• patient/Observation.rs — read and search Observations scoped to a single patient context established at launch • user/MedicationRequest.write — write access scoped to whatever patients the authenticated clinician is permitted to see under their EHR role • system/*.read — backend service account access for population-level Bulk Data jobs, using the SMART Backend Services (client_credentials grant with JWT client assertion) profile instead of a user-facing redirect

The authorization server is expected to enforce a least-privilege posture: even if an app requests patient/*.read, the server may downscope the grant to only the resource types it is willing to expose to that registered client, returning the actual granted scope in the token response so the app can adapt.

Token validation at the gateway

In production, the API gateway sitting in front of the FHIR resource server performs several checks on every inbound request before any FHIR processing occurs:

• Signature verification against the authorization server's published JWKS (JSON Web Key Set), confirming the token was minted by a trusted issuer • Expiry (exp claim) and not-before (nbf claim) checks — expired tokens are rejected with HTTP 401 and a WWW-Authenticate challenge header • Scope-to-operation matching — a token scoped to patient/Observation.rs attempting a POST to MedicationRequest is rejected with HTTP 403, not silently downgraded • Patient-context binding — the patient launch context claim is cross-checked against the patient compartment being queried, preventing a token issued for one patient from being replayed against another patient's record

This validation layer is what allows the gateway to reject malformed or expired requests before they consume backend FHIR server resources — visible in this simulation as red error packets that never reach the far node.

FHIR Resource Search, Read & Payload Transfer

With a valid bearer token in hand, the app now performs the actual clinical work: searching for and reading FHIR resources. Each resource — Patient, Observation, MedicationRequest — is a self-contained JSON document conforming to a US Core profile, transferred over standard REST semantics (GET for read/search, POST for create) through the API gateway to the target health system.

  • Patient, Obs, MedReq: Core resource types (+ Condition, Encounter)
  • LOINC, RxNorm, SNOMED CT: Coding systems used (per US Core bindings)
  • 0.8–6 KB: Typical resource size (JSON, uncompressed)
  • Bundle (type=searchset): Search response wrapper (paginated via next link)

Anatomy of a FHIR search request and response

A typical query — GET [base]/Observation?patient=1284&category=vital-signs&date=ge2026-01-01 — returns not a bare array but a Bundle resource of type searchset. The Bundle wraps each matching Observation in an entry with a fullUrl and a search.mode of "match", plus optional "include" entries for resources pulled in via _include (e.g. the referenced Patient). Pagination is handled through Bundle.link entries with relation "next" and "self", not custom offset parameters — a deliberate REST design choice that lets clients page through arbitrarily large result sets without the server needing to hold cursor state.

Each individual resource inside the Bundle carries a resourceType field, an id, a meta.profile array pinning it to the specific US Core StructureDefinition it conforms to (e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-blood-pressure), and clinical content encoded with standard terminologies:

• Observation.code — LOINC (e.g. 85354-9 for blood pressure panel) • MedicationRequest.medicationCodeableConcept — RxNorm • Condition.code — SNOMED CT (or ICD-10-CM for billing-oriented systems, requiring a crosswalk)

Payload complexity in this simulator scales with how many nested references, contained resources, and coded extensions each packet carries — richer clinical documentation means larger JSON cards moving through the gateway.

Read vs. search vs. instance-level operations

FHIR REST defines several distinct interaction types the gateway must route differently:

• read — GET [base]/Patient/1284 returns exactly one resource by known id, the simplest and lowest-latency interaction • vread — GET [base]/Patient/1284/_history/3 retrieves a specific historical version, supporting audit and point-in-time reconstruction • search-type — GET [base]/Observation?... returns a Bundle of zero or more matches, generally the most latency-variable operation since it touches an index or query planner on the backend • create — POST [base]/MedicationRequest with a JSON body creates a new resource, returning HTTP 201 with a Location header • update / patch — PUT or PATCH against a known id, often used for order status transitions (active → completed)

Hospital A and Hospital B in this simulation exchange resources bidirectionally: Hospital A both pulls Observations recorded at Hospital B and pushes newly created MedicationRequests back, modeling a realistic referral or transfer-of-care workflow rather than one-directional read replication.

Where payloads fail — validation and conformance errors

Not every request through the gateway succeeds. This stage introduces the first realistic error classes visible as red packets:

• HTTP 422 Unprocessable Entity — the resource is well-formed JSON but fails US Core profile validation, e.g. a MedicationRequest missing a required RxNorm code • HTTP 400 Bad Request — a malformed search parameter, such as an invalid date format that doesn't match FHIR's YYYY[-MM[-DDTHH:mm:ss[.sss+zz:zz]]] precision grammar • HTTP 404 Not Found — a reference to a resource id that has since been deleted or was never created on the target system, common when Hospital A caches stale identifiers • HTTP 429 Too Many Requests — the gateway's rate limiter rejecting a burst that exceeds the client's provisioned quota, previewing Stage 5

Production-grade gateways log every one of these with enough context (client_id, resource type, validation errors) to support debugging without exposing PHI in plaintext logs — a HIPAA Security Rule technical safeguard consideration baked into gateway design.

Bulk FHIR $export — Population-Scale Asynchronous Transfer

Individual REST reads do not scale to population health, quality measure reporting, or full patient-history migration. The Bulk Data Access (Flat FHIR) implementation guide defines the $export operation: an asynchronous, job-based pattern that produces newline-delimited JSON (NDJSON) files bundling thousands of resources per file, downloaded directly rather than paginated through individual Bundle responses.

  • $export: Export operation (system, group, or patient level)
  • NDJSON: File format (one resource per line, gzip-able)
  • HTTP 202 Accepted: Kickoff response (+ Content-Location poll URL)
  • 2–40 min: Typical job duration (scales with population size)

The asynchronous kickoff-poll-download pattern

Bulk Data export cannot be a synchronous request-response because exporting tens of thousands of resources may take minutes. The protocol instead uses a three-phase asynchronous pattern:

1. Kickoff — the client sends GET [base]/Group/[id]/$export (or /Patient/$export, or system-level /$export for a full population extract) with an Accept: application/fhir+json header and a Prefer: respond-async header. The server immediately responds HTTP 202 Accepted with no body, but includes a Content-Location header pointing to a status-polling URL.

2. Polling — the client periodically GETs the status URL. While the job runs, the server returns HTTP 202 with an X-Progress header. When complete, it returns HTTP 200 with a JSON manifest listing one or more output file URLs per resource type, each annotated with type (e.g. "Observation") and url.

3. Download — the client GETs each NDJSON file directly, often from object storage (S3-compatible) fronted by short-lived signed URLs rather than the FHIR server itself, keeping the resource server free to handle interactive traffic while bulk downloads stream separately.

This simulation renders bulk export as a surge of densely packed resource packets streaming through the gateway in a tight formation, distinct from the individually-routed packets of Stage 3.

The _since parameter on $export enables incremental extraction — a health system can request only resources changed after their last successful sync, turning what would be a full re-export into a delta feed suitable for nightly or hourly reconciliation jobs.

Why NDJSON instead of a giant Bundle

Bulk exports deliberately avoid wrapping everything in one giant FHIR Bundle resource. A Bundle containing 100,000 Observations would be a multi-gigabyte single JSON document — awkward to stream, awkward to parse incrementally, and a poor fit for typical JSON parsers that load a full document tree into memory.

NDJSON solves this: each line of the file is one complete, independently parseable FHIR resource as JSON. A consumer can stream-process the file line by line, load it directly into a data warehouse via bulk-insert tooling, or split it across parallel workers without ever holding the whole file in memory. Each output file is also scoped to a single resource type — one file for all exported Patient resources, another for all Observation resources — simplifying downstream ETL mapping to a research database, a data lake, or a CMS quality-measure reporting pipeline.

Governance: who can request a population export

Because $export can extract an entire population's clinical history in one operation, it carries proportionally higher governance requirements than a single-patient read:

• System-level export (/$export) is typically restricted to backend service accounts under SMART Backend Services authorization (system/*.read scope, JWT client-assertion grant, no user in the loop) — used for legitimate purposes like health information exchange population sync or a state public health registry feed • Group-level export (/Group/[id]/$export) scopes the extract to a defined cohort — a specific ACO's attributed patients, or a research study's enrolled participants — enforced by the FHIR server evaluating group membership before serving any data • Every bulk job kickoff, poll, and download is written to the audit log with the requesting client identity, the resource types and _type filters requested, and the resulting file manifest, satisfying HIPAA's accounting-of-disclosures obligations at population scale

TEFCA's Common Agreement explicitly contemplates Bulk Data exchange between Qualified Health Information Networks (QHINs) as one of its "Individual Access Services" and "Treatment" exchange purposes, formalizing bulk export as a first-class nationwide interoperability pattern rather than a vendor-specific extension.

Bulk export scope levels and typical use

ProductIndicationTrial DesignKey Result
Patient-level ($export)Single patient contextGET /Patient/[id]/$export — all resources referencing that patient compartmentPatient-facing app full-record download
Group-level (/Group/[id]/$export)Defined cohort / attribution listServer resolves Group membership, exports only matching patientsACO quality reporting, research cohorts
System-level (/$export)Entire population on the serverBackend service account, SMART Backend Services JWT authHIE population sync, registry feeds
Incremental (_since=)Resources changed since timestampDelta filter applied server-side before file generationNightly reconciliation without full re-export

Production Interoperability — Rate Limiting, Retries & Audit

A pilot integration handling a handful of test patients looks nothing like a production interoperability pipeline running continuously against live clinical traffic. At scale, the API gateway must gracefully absorb bursts, protect backend FHIR servers from overload, recover automatically from transient failures, and produce a defensible audit trail — all without silently dropping clinical data.

  • 20–30 req/s: Sustained throughput (per client, gateway-throttled)
  • Exponential backoff: Retry policy (+ jitter, max 5 attempts)
  • >50% errors / 30s: Circuit breaker trip (fails fast, sheds load)
  • 6+ years: Audit log retention (HIPAA accounting minimum)

Rate limiting — protecting shared infrastructure

Production FHIR gateways enforce per-client quotas to prevent any single integration from degrading service for every other trading partner sharing the same backend. This simulation models a token-bucket limiter: each client is allotted a steady refill rate of request tokens, can burst briefly above it using banked tokens, and receives HTTP 429 Too Many Requests with a Retry-After header once the bucket is empty.

Well-behaved clients treat 429 as a signal to slow down, not an error to retry immediately — a naive retry loop that ignores Retry-After can turn a brief throttle into a self-inflicted denial-of-service, worsening exactly the condition the rate limiter exists to prevent. Gateways also frequently tier limits by operation cost: a cheap Patient read by known id might be allowed at high frequency, while an expensive unbounded Observation search or a $export kickoff is throttled far more aggressively.

Retries, exponential backoff, and circuit breaking

Transient failures are a fact of distributed systems — a downstream database failover, a brief network partition, a backend FHIR server garbage-collection pause. Production gateways distinguish between errors worth retrying and errors that will never succeed on retry:

• Retryable: HTTP 429 (rate limited), 503 (temporarily unavailable), 502/504 (gateway/upstream timeout) — these get retried with exponential backoff plus random jitter (e.g. 200ms, 400ms, 800ms ± random offset) up to a bounded attempt count, preventing synchronized "thundering herd" retries from many clients at once • Non-retryable: HTTP 400 (malformed request), 401/403 (auth failure), 422 (validation failure) — retrying these wastes capacity and never succeeds, since the request itself is the problem, not transient infrastructure state

When a downstream system's error rate crosses a threshold (this simulation models >50% failures within a rolling 30-second window), the gateway's circuit breaker trips to the open state: it stops forwarding requests to that downstream entirely for a cooldown period, returning fast failures instead of piling up timeouts, then periodically allows a small number of trial requests through (half-open state) to test whether the downstream has recovered before fully closing the circuit again.

Circuit breaking exists to protect the struggling downstream, not just the caller — continuing to hammer an already-overloaded Hospital B FHIR server with retries can prevent it from ever recovering, turning a brief blip into an extended outage for every connected trading partner.

Audit logging and accounting of disclosures

Every request that reaches the gateway — successful or not — is written to an immutable, append-only audit log, typically itself modeled as a FHIR AuditEvent resource. A minimally compliant audit record captures:

• Who — the authenticated client_id and, where available, the underlying user or patient identity from the OAuth token • What — the resource type and operation (read, search, create, $export) and, for disclosures, which specific resources were returned • When — a precise timestamp, essential for reconstructing an incident timeline • Outcome — success, and if failed, the FHIR OperationOutcome detailing why (without leaking PHI into the outcome text itself) • Purpose of use — under TEFCA and many state HIE participation agreements, requests must declare a permitted purpose (Treatment, Payment, Operations, Public Health) that the audit trail preserves for later review

HIPAA's Privacy Rule accounting-of-disclosures provision (45 CFR §164.528) gives patients the right to request a list of certain disclosures of their health information going back six years — a right that is only practically fulfillable if every API-mediated exchange, including automated system-to-system Bulk Data transfers, is durably logged from day one rather than retrofitted after the fact.

At full production scale — the throughput and error mix simulated in this final stage — the gateway is simultaneously enforcing authorization, shaping traffic, recovering from failures, and building the evidentiary record that makes the whole exchange auditable. This is the operational reality behind every "the two systems just talk FHIR to each other" diagram.

⚙ Under the hood

A simulation tool for exchanging health data using the HL7 FHIR API standard.

CanvasBiomedicine

2D · HTML5 Canvas 2D · 60 FPS target · runs fully client-side, no install

What did you find?

Add reproduction steps (optional)