SMART on FHIR apps
Health Data Safe (HDS) exposes a read-only FHIR R4 API to SMART on FHIR apps. A patient launches your app, signs in to their HDS account, chooses what to share, and your app reads FHIR resources rendered from their vault. Nothing is copied: every request is read from the patient’s account with your app’s own token.
HDS conforms to SMART App Launch 2.2.0, tested with Inferno (details under Conformance and testing).
Regions and endpoints
Section titled “Regions and endpoints”Each patient’s data lives in one region, chosen when they created their account. The patient picks their region when they connect your app; your app then uses that region’s issuer and FHIR base for the whole session.
| Region | Issuer | FHIR base | Profiles |
|---|---|---|---|
| Switzerland | https://auth-ch.api.datasafe.dev | https://auth-ch.api.datasafe.dev/adapter/fhir/r4 | IPS 2.0.1 (International Patient Summary) |
| USA | https://auth-us.api.datasafe.dev | https://auth-us.api.datasafe.dev/adapter/fhir/r4 | US Core 6.1.0 |
| Sandbox | https://demo.datasafe.dev | https://demo.datasafe.dev/adapter/fhir/r4 | US Core 6.1.0 |
- A token issued in one region is refused by the other region’s FHIR server.
- Use the sandbox for development. Test patient accounts can be created freely there.
- A machine-readable list of the production endpoints is published as a SMART
User-access Brand Bundle at
https://healthdatasafe.github.io/assets-prod/smart/brands.json.
A patient’s own address
Section titled “A patient’s own address”Instead of asking for a region, your app may ask the patient for their HDS username and use
https://<username>.api.datasafe.dev/adapter/fhir/r4 as the FHIR base (iss). The address
leads straight to the patient’s region. It exists in production only, not in the sandbox.
- Discovery works on that base as on a region base. Its
smart-configurationnames the region’s issuer and OAuth endpoints: take them from there. audmust be that same base. At sign-in HDS checks that the name is the signed-in patient’s own; another account’s address is refused withaccess_denied.- A token issued for a patient’s own address is accepted there only, not on the region base, and a region token is not accepted on the patient’s address.
fhirUserin the id_token, and every link in the responses, are on the patient’s address.- Your app learns the patient’s username, which the patient chooses to give it. If your app should not hold it, use the region bases.
Discovery
Section titled “Discovery”GET <FHIR base>/.well-known/smart-configuration: the SMART configuration, includingscopes_supportedandcapabilities.GET <FHIR base>/metadata: the CapabilityStatement, with the profiles each resource type claims on that endpoint (supportedProfile).- The issuer also serves
/.well-known/openid-configurationand/.well-known/oauth-authorization-server, and a JWKS (jwks_uri) to verify id_tokens. - The OAuth endpoints are on the issuer host:
/oauth2/authorize,/oauth2/tokenand/oauth2/revoke. Always take them from the discovery document rather than hard-coding them.
What is supported
Section titled “What is supported”Launch and authorization
- SMART App Launch 2.2.0 standalone patient launch (
launch/patient). - Authorization code flow with PKCE
S256, required for every client (plainis not offered). - Public clients, and confidential clients authenticating with
client_secret_basicor withprivate_key_jwt(RS384orES384assertions). audis required on the authorize request and must be the FHIR base your app uses: the region’s, or the patient’s own address (above).- The authorize request may be sent as GET or POST.
Scopes
- SMART v2 resource scopes
patient/<Type>.rs(read and search).patient/*.rsasks for every type. The v1 formpatient/<Type>.readis accepted too. - A v2 scope may carry one
?category=to narrow it, for examplepatient/Observation.rs?category=vital-signs. offline_accessfor a refresh token.openid fhirUserfor an id_token: signedRS256, valid 300 seconds, withfhirUserset to the patient’s absolute URL (<FHIR base>/Patient/<id>).hds/pseudonym(HDS-specific): the patient id your app receives is a pseudonymous alias rather than the patient’s HDS user id.- Nothing writes: no
.c,.uor.dscope is granted. - The
scopereturned with the token is what the patient actually granted. Read it rather than assuming your request was granted as asked. The full list of scopes a region offers is itsscopes_supported.
Resources and search
| Resource | Interactions | Search parameters |
|---|---|---|
Patient | read, search | _id |
Observation | read, search | patient, category, code, date; _include=Observation:device; _revinclude=Provenance:target |
Condition | read, search | patient, category, code; _revinclude=Provenance:target |
Provenance | read | (reached through _revinclude) |
- Observation categories served: vital signs, laboratory, survey and social history.
- Search also works as
POST <Type>/_searchwith anapplication/x-www-form-urlencodedbody. datetakes the prefixeseq,ge,gt,leandlt, at year, month, day or date-time precision._countdefaults to 50, with a maximum of 200. Follow the bundle’snextlink for more.patient(orsubject) must be the patient in your token; another id is refused with 403.Patient/<id>reads only that patient.- Responses are
application/fhir+jsononly. CORS is open, so browser apps can call the API directly. - Errors are returned as an
OperationOutcome. A missing, inactive or wrong-audience token gets 401 with aWWW-Authenticateheader.
Rate limit. Authenticated requests are limited to 600 per minute per client IP address, with
bursts up to 120. Above it the server answers 429 with a Retry-After header.
What is not supported
Section titled “What is not supported”HDS does not offer
- EHR launch (
launch), or any persona other than the patient; - backend services (
system/scopes) or bulk data export; - writes of any kind;
- DPoP-bound tokens;
- dynamic client registration (see Registering your app).
Consent and revocation
Section titled “Consent and revocation”- At launch the patient sees what your app asks for, grouped by category, and can narrow it per data stream before accepting. Access is always read-only.
- A grant has no expiry: it lives until the patient revokes it in their HDS account.
- Revocation takes effect immediately. The FHIR server checks every request with the authorization server and never caches an active token, so the next call after a revocation gets 401.
- Your app can revoke its own tokens at the issuer’s
/oauth2/revoke. - Everything the patient consents to is readable. Data that has no FHIR mapping yet is served
as a generic
Observation, tagged inmeta.tagwith systemhttps://hds.ngo/fhir/CodeSystem/rendering, codegeneric. It follows no profile, carries an HDS-only category (hds-item), and is returned by an Observation search without acategory, never by one with a category.
Pairwise identifiers and linkability
Section titled “Pairwise identifiers and linkability”Third-party apps are registered pairwise. The patient id and the API address your app
sees are unique to your app, so two apps cannot match a patient by id.
Pairwise does not unlink the data itself. Resource ids are the vault’s record ids, and they are the same for every app the patient shares a record with. Two apps that both read the same record could therefore link their pseudonyms through it, as they could through the content and timestamps of the data. HDS accepts this and states it here; publishers accept this statement in writing when they register.
Registering your app
Section titled “Registering your app”Registration is curated by HDS: there is no dynamic registration, on the sandbox or in production. To register, write to HDS at tech@healthdatasafe.org with:
- Who you are: the legal entity, a contact address, and the person accountable for the app.
- What the app does with the data: its purpose, whether data leaves the device, retention, onward sharing and hosting region; the URLs of your privacy policy and terms of use.
- Redirect URIs: each one
httpson a host you control, or an RFC 8252 loopback form for a native app. Plainhttpredirects are not accepted. - Scopes: the smallest set the app needs. Ask for
offline_accessonly if the app really refreshes tokens. - Client type: a browser or mobile app is a public client (PKCE is mandatory). A
server-side app uses
private_key_jwtwith a JWKS URL;client_secret_basicis discouraged for third parties. - What the patient sees: the app name (localized names optional), a one-line description shown at consent, and the app’s website.
- Your written acceptance of the linkability statement.
HDS vets the application and, once approved, registers the app pairwise for both regions,
so a patient in either region can use it, at the region bases and at the patient’s own address.
You receive your client_id, the issuer and FHIR base of each region, and the support contact.
After registration
- Changing a vetted field (redirect URIs, scopes, audiences, key set, authentication method, grant types, token lifetimes, offline access, launch type) puts the registration under review, and the app stops working until HDS approves the change. Plan changes ahead with HDS.
- If what the app may be granted is narrowed, existing grants are withdrawn and patients consent again.
- HDS may suspend an app (for example during an incident or an open vetting question): it is refused within 30 seconds, and patients’ sessions survive for when it resumes.
- An app removed for good has every grant withdrawn in both regions before its registration is revoked.
Worked example (sandbox)
Section titled “Worked example (sandbox)”The examples use the sandbox; swap in the region’s issuer and FHIR base for production. Your
client_id and redirect URI come from your registration.
1. Discover
curl https://demo.datasafe.dev/adapter/fhir/r4/.well-known/smart-configurationRead authorization_endpoint, token_endpoint, scopes_supported and capabilities from the
answer.
2. Send the patient to the authorize endpoint
Generate a random state and a PKCE code_verifier, then redirect the browser to
authorization_endpoint with these parameters (shown decoded; URL-encode them in the query):
response_type=codeclient_id=<your client id>redirect_uri=https://app.example.com/callbackscope=launch/patient openid fhirUser patient/Patient.rs patient/Observation.rsstate=<random>aud=https://demo.datasafe.dev/adapter/fhir/r4code_challenge=<BASE64URL(SHA256(code_verifier))>code_challenge_method=S256The patient signs in, reviews the request and accepts. The browser comes back to your
redirect_uri with code and state; check that state matches.
3. Exchange the code for a token
curl -X POST https://demo.datasafe.dev/oauth2/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d grant_type=authorization_code \ -d code=<code> \ -d redirect_uri=https://app.example.com/callback \ -d client_id=<your client id> \ -d code_verifier=<code_verifier>The answer carries access_token, token_type (Bearer), expires_in, the granted scope,
patient (the patient id to use in your requests) and, with openid fhirUser, an id_token.
A confidential client authenticates here as registered.
4. Read the patient and their observations
curl -H 'Authorization: Bearer <access_token>' -H 'Accept: application/fhir+json' \ https://demo.datasafe.dev/adapter/fhir/r4/Patient/<patient>
curl -H 'Authorization: Bearer <access_token>' -H 'Accept: application/fhir+json' \ 'https://demo.datasafe.dev/adapter/fhir/r4/Observation?patient=<patient>&category=vital-signs&_count=20'The search returns a searchset Bundle; follow its next link for further pages.
With fhirclient
Section titled “With fhirclient”The SMART project’s own JavaScript library, fhirclient 2.x, works unmodified against the sandbox (tested on 2026-11-04 with a standalone launch). It runs discovery, PKCE and the token exchange for you:
// launch pageFHIR.oauth2.authorize({ clientId: '<your client id>', iss: 'https://demo.datasafe.dev/adapter/fhir/r4', scope: 'launch/patient openid fhirUser patient/Patient.rs patient/Observation.rs', redirectUri: './callback.html'});
// callback pageconst client = await FHIR.oauth2.ready();const patient = await client.patient.read();const vitals = await client.request(`Observation?patient=${client.patient.id}&category=vital-signs`);Conformance and testing
Section titled “Conformance and testing”HDS conforms to SMART App Launch 2.2.0, tested with Inferno.
What was run, precisely: on 2026-11-04 the Inferno SMART App Launch STU2.2 test kit and the Inferno US Core 6.1.0 test kit were run against the sandbox.
- SMART App Launch: every test passed for the standalone launch (with OpenID Connect and token
refresh) with a public client and with a pairwise client, and for token introspection. With a
confidential
client_secret_basicclient, every test run passed; the kit omits one CORS test for non-public clients. - US Core 6.1.0: no test failed on a resource type HDS serves. Some of those tests were skipped for lack of sample data. Tests for resource types HDS does not hold fail, as those types are absent from the CapabilityStatement.
The Inferno kits were not run against the production regions, and they do not cover the Swiss region’s IPS rendering. HDS holds no ONC certification.