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, the page opens the picker, they pick their health app, review and share data. The redirect return lands on 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 OID4VP direct_post.jwt response endpoint tells it what to do next:

  • Response includes redirect_uri: navigate there; in this demo it carries #response_code=....
  • No response redirect_uri: show completion in the source app while the requester retrieves the result through its own application path.

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 one custom Credential Query: id: "smart-checkin" and format: "smart_health_checkin". That DCQL item carries an exact SMART Health Check-in clinical request at meta.request; clinical asks live in items[].

Profile Wire Contract

The OID4VP shell always has one smart-checkin Credential Query. The response is keyed by that id as vp_token["smart-checkin"], whose value is a one-element Presentation array containing the SMART clinical response object. If we ever split items into multiple DCQL queries or wrap the SMART response differently, the relay, source apps, requester validation, tests, and this explainer must all change together.

FHIR Resources

Structured clinical data — insurance cards, plan benefit summaries, patient demographics, lab results, medications. Requested with selection.fhir selectors.

"content": { "kind": "selection.fhir", "profiles": ["http://hl7.org/fhir/us/insurance-card/StructureDefinition/C4DIC-Coverage"] }
"content": { "kind": "selection.fhir", "profilesFrom": ["http://hl7.org/fhir/us/core"], "resourceTypes": ["Patient"] }

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.

"content": { "kind": "form.fhir", "questionnaireCanonical": "https://smart-health-checkin.example.org/fhir/Questionnaire/chronic-migraine-followup", "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. The item requests acceptable artifact media types through accept[].

"accept": ["application/smart-health-card", "application/fhir+json"]

A full request uses one DCQL credential query and lists clinical items inside the SMART request:

{ "credentials": [ { "id": "smart-checkin", "format": "smart_health_checkin", "require_cryptographic_holder_binding": false, "meta": { "request": { "type": "smart-health-checkin-request", "version": "1", "id": "checkin-request-123", "purpose": "Clinic check-in", "fhirVersions": ["4.0.1"], "items": [ { "id": "coverage", "title": "Insurance card", "content": { "kind": "selection.fhir", "profiles": ["...C4DIC-Coverage"] }, "accept": ["application/fhir+json"] }, { "id": "intake", "title": "Migraine follow-up", "content": { "kind": "form.fhir", "questionnaireCanonical": "https://.../Questionnaire/chronic-migraine-followup" }, "accept": ["application/fhir+json"] } ] } } } ] }

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", "dcql_query": { // carries the SMART clinical request "credentials": [ { "id": "smart-checkin", "format": "smart_health_checkin", "require_cryptographic_holder_binding": false, "meta": { "request": { "type": "smart-health-checkin-request", "version": "1", "id": "checkin-request-123", "purpose": "Clinic check-in", "fhirVersions": ["4.0.1"], "items": [ { "id": "coverage", "title": "Insurance card", "content": { "kind": "selection.fhir", "profiles": ["...C4DIC-Coverage"] }, "accept": ["application/fhir+json"] }, { "id": "intake", "title": "Migraine follow-up", "content": { "kind": "form.fhir", "questionnaireCanonical": "https://.../Questionnaire/chronic-migraine-followup" }, "accept": ["application/fhir+json"] } ] } } } ] }, "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 signed Request Object does not include redirect_uri; same-device continuation is returned later by the response endpoint after the encrypted Wallet POST.

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 a DCQL vp_token keyed by the credential query id. Each value is an array of Presentations; our custom profile uses one SMART Health Check-in response object as that Presentation. There is no presentation_submission field in this DCQL response:

{ "state": "req_abc123", // validated against expected value "vp_token": { "smart-checkin": [{ "type": "smart-health-checkin-response", "version": "1", "requestId": "demo-smart-health-checkin", "artifacts": [{ "id": "art_0", "mediaType": "application/fhir+json", "fhirVersion": "4.0.1", "fulfills": ["coverage"], "value": { "resourceType": "Coverage", ... } }], "requestStatus": [ { "item": "coverage", "status": "fulfilled" } ] }] } }

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.

Same-device completion exists to bind the encrypted response to the verifier tab that initiated the request. The W3C Digital Credentials API will eventually route the wallet response back to that originating frame natively. Until then, this demo uses a server-mediated stand-in: the relay returns a response_code through a verifier-controlled redirect, and the originating tab exchanges that code for the encrypted payload.

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 the picker. The reference portal uses a same-tab handoff; the shim also supports a popup handoff for compatibility.
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 redirect lands back on the Verifier's page with #response_code=.... In same-tab mode, the landing page resumes the requester session and redeems the response directly. In popup mode, the return page signals the opener before closing.
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, validates the SMART response against the original SMART request, and groups returned Artifacts by request item for 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 continuation redirect_uri is stored for the response endpoint 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 how the direct_post.jwt response endpoint completes: same-device returns a continuation redirect_uri with a fresh response_code; cross-device returns an acknowledgement and the kiosk retrieves the result through its authenticated session. 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 OID4VP direct_post.jwt response endpoint tells the source app what to do immediately after posting the encrypted response. The SMART clinical request body stays limited to requested health artifacts and forms.

  • Redirect URI: The response endpoint can return redirect_uri after it successfully processes the wallet POST. The source app follows it if present.
  • 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.

Shim API

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

const completion = await completeSameDeviceRedirect();
if (completion) renderResult(completion.response);
else await maybeHandleReturn();

function startCheckin(smartRequest) {
  void request(smartRequest, {
    walletUrl: 'https://picker.example.com',
    wellKnownClientUrl: 'https://clinic.example.com',
    flow: 'same-device',   // or 'cross-device'
    sameDeviceLaunch: 'replace',
    onRequestStart(info) {
      // info.bootstrap  — { client_id, request_uri }
      // info.launch_url — full URL for same-tab launch, popup, or QR
      // info.transaction — implementation-specific request tracking details
    }
  });
}

// completion.response.credentials.coverage[0] → FHIR Coverage resource

Both flows use the same request() call. Same-device can use a same-tab or popup handoff; 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 run demo. The selected JSON profile in deployments/ controls the hosted URLs and share-sheet apps.