SMART Health Check-in

A standards-based protocol for secure health data sharing

What is this?

SMART Health Check-in is an open protocol that lets patients share health records from their personal health apps directly with healthcare providers. It uses OpenID for Verifiable Presentations (OID4VP) with end-to-end encryption, signed request objects, and support for both same-device and cross-device flows. A browser shim makes it easy to use from web applications, but the underlying protocol works from any environment that can make HTTP requests and handle JWE encryption.

This page is a visual overview. For the full specification, see the protocol README. To try it live, visit the interactive demo.

Same-Device (Patient Portal)

Patient is on their own device. They click a button, a popup opens, they pick their health app, review and share data. The popup returns to the portal page with a response_code to close the loop. The reference portal does not offer a patient-initiated QR mode.

Cross-Device (Front Desk Kiosk)

Authenticated clinic staff starts a check-in at a workstation. A QR code appears. The patient scans it on their phone, picks their health app, and shares data. The kiosk receives the encrypted result via long-poll.

Both flows use the same protocol stack, the same shim library, the same picker, the same wallet/source app, and the same verifier backend.

What the source app needs to know

After the source app posts the encrypted response, the request tells it which completion shape to expect:

  • completion: "redirect": expect a redirect_uri with #response_code=..., then navigate there.
  • completion: "deferred": expect a simple success acknowledgement, then show completion in the source app.

Roles

Verifier The healthcare provider requesting data. Controls the well_known: origin, hosts metadata and signing keys, signs Request Objects, stores encrypted responses. The browser-side shim library (smart-health-checkin) is part of the Verifier's frontend. In the demo: the relay/backend server + the portal or kiosk page.
Picker An optional routing UI where the patient chooses which health app to use. Receives only the minimal bootstrap parameters and forwards them. In the demo: the check-in page.
Wallet / Source App The patient's health app that holds their data. Verifies the signed Request Object, collects consent, encrypts the response, and POSTs it to the Verifier's response endpoint. In the demo: Sample Health App.

What Can Be Requested

Requests use the DCQL query language with a custom credential format called smart_artifact. Three kinds of health data can be requested through the meta field:

FHIR Resources

Structured clinical data — insurance cards, plan benefit summaries, patient demographics, lab results, medications. Requested by canonical StructureDefinition URL.

"meta": { "profile": "http://hl7.org/fhir/us/insurance-card/StructureDefinition/C4DIC-Coverage" }
"meta": { "profile": "http://hl7.org/fhir/us/insurance-card/StructureDefinition/sbc-insurance-plan" }

Questionnaires

Forms the patient fills out — symptom checklists, disease-specific diaries, visit goals. The health app can auto-fill answers from patient-reported records and return a FHIR QuestionnaireResponse.

"meta": { "questionnaire": { "resourceType": "Questionnaire", "title": "Migraine Check-in", "item": [ { "linkId": "follow-up", "text": "Since your last migraine follow-up", "type": "group", "item": [ { "linkId": "migraine-days-90", "type": "integer", "required": true }, { "linkId": "overall-change", "type": "choice", "answerOption": [ ... ] }, { "linkId": "visit-priority", "type": "text", "required": true } ] }, { "linkId": "medication-use-note", "type": "display", "enableWhen": [{ "question": "acute-med-days-30", "operator": ">", "answerInteger": 9 }] } ] } }

SMART Health Cards

Cryptographically signed credentials — vaccination records, lab results with issuer signatures. The verifier can independently verify the issuer's signature. Requested by combining a FHIR profile with a signingStrategy.

If signingStrategy is omitted, the wallet may return any format. If present, it lists acceptable strategies: ["shc_v1", "none"] = prefer signed, accept unsigned. ["shc_v1"] = SHALL be signed.

"meta": { "profile": "http://hl7.org/fhir/StructureDefinition/Immunization", "signingStrategy": ["shc_v1", "none"] // prefer signed, accept unsigned }

A full query combines these into a credentials array. Use credential_sets with required: false to mark items as optional:

{ "credentials": [ { "id": "req_insurance", "format": "smart_artifact", "meta": { "profile": "...StructureDefinition/C4DIC-Coverage" } }, { "id": "req_plan", "format": "smart_artifact", "meta": { "profile": "...StructureDefinition/sbc-insurance-plan" } }, { "id": "req_intake", "format": "smart_artifact", "meta": { "questionnaire": { ... } } }, { "id": "req_immunization", "format": "smart_artifact", "meta": { "profile": "...Immunization", "signingStrategy": ["shc_v1"] } } ], "credential_sets": [ { "options": [["req_insurance"]], "required": false }, { "options": [["req_plan"]], "required": false }, { "options": [["req_intake"]], "required": false }, { "options": [["req_immunization"]], "required": false } ] }

The Request: From URL to Credentials

A SMART Health Check-in request is a series of expansions. A compact bootstrap URL resolves into a signed JWT, which contains the full request parameters including the ephemeral encryption key. Here's what each layer looks like:

Bootstrap URL shown in popup or QR

The URL the patient sees is intentionally minimal — just three parameters. This keeps QR codes small.

client_id=well_known:https://clinic.example.com request_uri=https://clinic.example.com/oid4vp/requests/abc123
client_id resolves to Verifier Metadata

The wallet strips the well_known: prefix to get the origin, then fetches /.well-known/openid4vp-client:

{ "client_id": "well_known:https://clinic.example.com", "jwks_uri": "https://clinic.example.com/.well-known/jwks.json", "client_name": "General Hospital Cardiology", // display only — not trusted without allowlist "request_object_signing_alg_values_supported": ["ES256"] }
request_uri resolves to a Signed Request Object

The wallet fetches request_uri and gets a signed JWT. It verifies the signature using keys from jwks_uri. This is where all the real parameters live:

{ "iss": "well_known:https://clinic.example.com", "aud": "https://self-issued.me/v2", "client_id": "well_known:https://clinic.example.com", // SHALL match bootstrap "response_type": "vp_token", "response_mode": "direct_post.jwt", "response_uri": "https://clinic.example.com/oid4vp/responses/wt_7f3a...", "state": "req_abc123", "nonce": "n-0S6_WzA2Mj", "smart_health_checkin": { "completion": "redirect" // or "deferred" }, "dcql_query": { // what credentials to request (see below) "credentials": [ { "id": "req_insurance", "format": "smart_artifact", "meta": { "profile": "...C4DIC-Coverage" } }, { "id": "req_plan", "format": "smart_artifact", "meta": { "profile": "...sbc-insurance-plan" } }, { "id": "req_intake", "format": "smart_artifact", "meta": { "questionnaire": { ... } } } ], "credential_sets": [ { "options": [["req_insurance"]], "required": false }, { "options": [["req_plan"]], "required": false }, { "options": [["req_intake"]], "required": false } ] }, "client_metadata": { "jwks": { "keys": [{ // ephemeral encryption key for THIS request "kty": "EC", "crv": "P-256", "use": "enc", "alg": "ECDH-ES", "x": "...", "y": "..." }]}, "encrypted_response_enc_values_supported": ["A256GCM"] } }

Because the JWT is signed by the Verifier's key, every value here — including response_uri — is authenticated by the signature. No separate metadata check is needed.

The Response: Encrypted, Then Decrypted

Wallet encrypts and POSTs direct_post.jwt

The wallet builds { vp_token, state }, encrypts it as a JWE using the ephemeral key from the Request Object, and POSTs it to response_uri. In this demo, the relay stores only opaque ciphertext; another Verifier implementation could deliver the encrypted response directly to a component that can decrypt it.

First-write-wins: the first POST is authoritative. Retries with the same JWE are idempotent. A different payload is rejected with 409.
Verifier decrypts the JWE

Only a Verifier component that holds the ephemeral private key can decrypt. In this demo, that key is held by the Verifier browser. The result contains vp_token with inline references for deduplication:

{ "state": "req_abc123", // validated against expected value "vp_token": { "req_insurance": [{ "artifact_id": "cov_1", "type": "fhir_resource", "data": { "resourceType": "Coverage", "subscriberId": "W123456789", ... } }], "req_plan": [{ "type": "fhir_resource", "data": { "resourceType": "InsurancePlan", "name": "Aetna PPO Value Plan", ... } }], "req_patient": [{ "artifact_ref": "cov_1" // references the same artifact — no duplication }, { "type": "fhir_resource", "data": { "resourceType": "Patient", ... } }] } }

This profile authenticates the request and provides encrypted transport, but does not prove artifact provenance. Unless an artifact carries its own verifiable proof (e.g., a SMART Health Card), it should be treated as a transaction-bound submission.

Same-Device Flow

Patient is on their own device — portal, patient app, or third-party site.

Verifier Generate the Request
The Verifier backend creates a transaction and a signed Request Object. The browser generates an ephemeral ECDH key pair — the private key stays in memory, the public key is embedded in the signed Request Object so the wallet can encrypt the response.
Shim Deliver the Request
The shim starts a same-device transaction and receives a request_uri. It builds a bootstrap URL with just client_id and request_uri, then opens a popup to the picker.
In future, the W3C Digital Credentials API will handle this natively.
Wallet Verify, Consent, Respond
The wallet (via a picker) receives the bootstrap URL. It fetches /.well-known/openid4vp-client from the verifier origin to get the jwks_uri. It fetches request_uri to get the signed Request Object JWT, verifies the signature, and extracts response_uri, dcql_query, and the ephemeral encryption key.

After the user reviews and consents, the wallet encrypts { vp_token, state } as a JWE and POSTs it to response_uri. The server returns { redirect_uri: "portalPage#response_code=..." }. The wallet navigates there.
Shim Complete the Exchange
The popup lands back on the Verifier's page with #response_code=.... The Verifier app uses that completion signal to obtain the encrypted response through its own application path.
In future, the W3C Digital Credentials API will handle this natively.
Verifier Receive the Response
The shim decrypts the JWE using the ephemeral private key, validates that state matches the expected value, resolves any artifact_ref inline references, and returns the credentials to the application.

Cross-Device Flow

Staff starts the check-in at a workstation. Patient completes it on their phone.

Verifier Generate the Request
Same as same-device: transaction created, ephemeral key pair generated. The difference is that no redirect_uri is needed because the source app will not navigate back to the kiosk.
Shim Display QR Code
No popup is opened. Instead, onRequestStart provides the launch_url to the app, which renders it as a QR code and copyable link. The Verifier app waits for the response through its own application path.
In future, the W3C Digital Credentials API will handle this natively.
▼ patient scans QR on their phone
Wallet (patient's phone) Verify, Consent, Respond
Identical to same-device: picker → source app → verify signed Request Object → consent → encrypt → POST JWE to response_uri. Server returns { status: "ok" } (no redirect). Source app shows "Submission complete."
Shim Long-Poll Resolves
The Verifier app receives the encrypted response. No response_code is returned to the source app in this mode.
In future, the W3C Digital Credentials API will handle this natively.
Verifier Receive the Response
Same decryption and rehydration as same-device. The kiosk displays the received credentials.
The protocol-facing distinction is the completion shape: same-device uses completion: "redirect"; cross-device uses completion: "deferred". Session binding and result retrieval are Verifier implementation details.

Security Model

Protocol Guarantees

  • Verifier authentication: well_known: identity + signed Request Objects prove the request is controlled by the domain that owns the verifier origin.
  • Encrypted response transport: Responses are encrypted using the ephemeral key from the signed Request Object. The reference relay stores only opaque ciphertext, but the protocol also allows a Verifier endpoint that can decrypt directly.
  • No blind trust of metadata: Display names and logos from /.well-known/openid4vp-client SHALL NOT be shown as trusted identity without an out-of-band allowlist.
  • Artifact authenticity is not guaranteed: The protocol authenticates the request and encrypts the transport, but does not prove who created the returned data. Unless an artifact carries its own verifiable proof (e.g., a SMART Health Card with an issuer signature), it should be treated as a transaction-bound submission.
  • Downstream reveal requires binding checks: Before decrypted data is shown to a user, persisted as clinical data, or shared with another system, the Verifier SHALL check that the completion belongs to the expected transaction and application session. Redirect completion binds the response_code to the initiating session; deferred completion requires an authenticated Verifier session authorized for that transaction.

Completion Behavior

The protocol tells the source app what to do immediately after posting the encrypted response. The Verifier decides how to store, retrieve, and bind that response inside its own application.

  • Completion hint: The signed Request Object tells the source app whether to expect a redirect with response_code or a simple acknowledgement after it posts the encrypted response.
  • Deployment controls: Session binding, result retrieval, subject matching, and other anti-phishing controls are handled by the Verifier implementation before decrypted data is revealed downstream. The protocol requires the completion hint so clients know how to finish the POST.

Shim API

import { request, maybeHandleReturn } from 'smart-health-checkin';

const result = await request(dcqlQuery, {
  walletUrl: 'https://picker.example.com',
  wellKnownClientUrl: 'https://clinic.example.com',
  flow: 'same-device',   // or 'cross-device'
  onRequestStart(info) {
    // info.bootstrap  — { client_id, request_uri }
    // info.launch_url — full URL for popup or QR
    // info.transaction — implementation-specific request tracking details
  }
});

// result.credentials['coverage-1'][0] → FHIR Coverage resource

Both flows use the same request() call. The difference is in what the app does with onRequestStart: same-device ignores it (the shim opens the popup), cross-device renders info.launch_url as a QR code.

Demo Architecture

PortalSame-device demo. Calls shim with flow: 'same-device'; no patient-portal QR mode.
KioskStaff-session-bound cross-device demo. Staff login, QR code, long-poll. flow: 'cross-device'.
PickerShared routing page. Receives bootstrap params, forwards to source app.
Source (Sample Health App)Mock wallet. Fetches metadata, verifies signed Request Object, collects consent, encrypts and POSTs response.
Relay / VerifierBackend. Serves .well-known, signs Request Objects, stores encrypted responses, enforces session binding for cross-device.

Everything runs on a single port via bun demo/serve-demo.ts. For local development, ./start-local.sh starts multi-origin servers to simulate real cross-origin security.