Wallet guide

In this section

A wallet answers a check-in request with records and form answers the patient chose to share. @smart-health-checkin/client/wallet has the protocol and the matching rules; the consent screen is yours. Install the package, or load the hosted wallet.js.

What a wallet does

  1. Receives a request, from the Digital Credentials API (native) or, for a web wallet, from the check-in page that opened it.
  2. Shows the patient who is asking and what for. Say only what the wallet knows: "A website is asking for your health information" with the requesting page's origin shown prominently, or, for a native app calling directly, "An app is asking for your health information". Never call the requester a practice, clinic, doctor, or provider: nothing in the request proves that.
  3. Lets the patient choose, item by item.
  4. Builds a SMART Health Check-in response: one status per item, plus artifacts.
  5. Signs and encrypts the response for the requesting page's origin, and sends it back.

The spec covers each step: request handling, the response model, and encryption.

Native wallets on Android

A native wallet registers with Android's Credential Manager and answers requests the browser passes through the Digital Credentials API.

Native wallets on iOS

On iOS 26, a wallet answers Safari through an Identity Document Provider extension. Apple has to approve the org.smarthealthit.checkin.1 document type for the app's entitlement. The extension can read the SMART request (requestInfo) only once the patient interacts, inside sendResponse; it can hold that callback open while it shows its own item-by-item screens, then answer. The Swift package implements both sides, and Platform notes has the details, including stripping the trailing slash from the origin Safari reports.

The rest of this page applies to both kinds: the matching rules, forms, statuses, and health cards are the same.

Web wallets

A web wallet is a page that the check-in page, the Verifier, opens in a tab. serveWebWallet handles the hand-off:

import { declineAll, serveWebWallet } from "@smart-health-checkin/client/wallet";

const served = serveWebWallet({
  async onRequest({ request, origin, unsupportedItems }) {
    const choice = await showConsentScreen(request, origin, unsupportedItems); // your UI
    if (choice.kind === "closed") return { declined: true };              // closed without reviewing
    if (choice.kind === "declined-all") return { response: declineAll(request) }; // reviewed, shared nothing
    if (choice.kind === "failed") return { error: "Couldn't read your records" };
    return { response: choice.response }; // checked against the request, then signed, encrypted, and sent
  },
  onInvalidRequest(message, origin) {
    showError(`${origin} sent a request this wallet can't read: ${message}`);
  },
});

if (!served.opened) showLandingPage(); // opened directly, not by a check-in page

What happens under it (Web wallets has the details):

Step Message
The Verifier opens your page in a tab none
Your page says it's ready ready, to the opener
The Verifier sends the request request, with the Digital Credentials API argument
You answer response: approved with a credential, declined, or an error

onRequest returns one of four answers:

Answer What the Verifier gets
{ response } Your SMART response, signed and encrypted for the Verifier's origin. It's checked against the request first; a response a Verifier would set aside (a status missing, a record in a type the item doesn't accept) becomes an error reply instead.
{ declined: true } The patient closed the wallet without reviewing. If they reviewed and declined everything, send { response: declineAll(request) } instead (HOLD-4).
{ error: "…" } An error with your message
{ credential } A credential you sealed yourself, sent as is. For test wallets that inject faults.

Options:

Option Default What it does
onRequest required Show consent and return an answer
onInvalidRequest none Called when a request can't be read. The Verifier also gets an error reply.
closeAfterReply true Close the tab after replying

Matching records to items

A selection.fhir item names records by profile or resource type (§5.4.1). selects tests one resource; selectEntries picks from a Bundle's entries.

import { selectEntries } from "@smart-health-checkin/client/wallet";

const entries = selectEntries(item.content, patientBundle.entry, { exclude: [patientFullUrl] });

The rules:

Selector Matches
profiles Resources whose meta.profile has that canonical
profiles with |version Only that exact version
profiles without a version Any version
profilesFrom Any profile under that family's URL
profiles and profilesFrom together Either one (they add up)
resourceTypes Narrows the above; alone, selects by type
no selector Everything

selectEntries also returns the entries a match references, such as a prescriber or a payer, so references in your Bundle resolve. Pass the Patient's fullUrl in exclude when the Patient has its own item.

Version handling is in §5.5.

Forms

A form.fhir item asks for a QuestionnaireResponse (§5.4.2).

Statuses

Every item gets exactly one status (§6.2).

Status When
fulfilled You returned what was asked
partial You returned some of it
unavailable The patient has nothing that matches
declined The patient chose not to share it
unsupported Your wallet can't answer this kind of item, including selector kinds it doesn't know (§5.4.3)
error Something went wrong for this item

One artifact can fulfill several items: list them all in its fulfills (§6.3).

SMART Health Cards

When an item lists application/smart-health-card first in accept and the patient has a card for it, return the card (§5.6).

Lower-level pieces

For wallets that seal their own responses, or run somewhere serveWebWallet doesn't fit:

Function What it does
parseWalletRequest(navigatorArgument) The SMART request, the items to answer unsupported, warnings about the request's wire format, and the pieces needed to answer. Throws WalletRequestError only where spec §8.4 says not to respond.
checkWalletResponse(request, response) What a Verifier would object to in your response; empty when it's clean
declineAll(request) The response for a patient who reviewed and declined everything
sealWalletResponse({ smartResponse, encryptionInfoBytes, verifierOrigin, request? }) Sign and encrypt a response; returns the credential to send. With request, checks the response first.
buildSignedDeviceResponse({ smartResponseJson, sessionTranscript }) The signed mdoc DeviceResponse, before encryption
recipientJwkFromEncryptionInfo(encryptionInfoBytes) The Verifier's public key

A reference to compare against

The SMART Testing Wallet implements all of this with serveWebWallet and selectEntries, plus switches for sending deliberately broken responses. Its features page lists exactly what it does.

Test your wallet against the Testing EHR: it sends every connectathon scenario and checks your answer against the spec. See Testing.

To check your bytes offline, the spec publishes conformance fixtures: real captured requests and responses, with every layer decoded. They're in the spec repository at tag v1.0.0-draft.2, along with small conformance cases your implementation can run in CI, and the capture inspector walks them byte by byte.

Getting listed

Check-in pages offer web wallets from a registry, a wallets.json file. To appear in one: